@timber-js/app 0.2.0-alpha.77 → 0.2.0-alpha.78

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.
@@ -45,10 +45,6 @@ function getStore() {
45
45
  function setLinkForCurrentNavigation(link) {
46
46
  const store = getStore();
47
47
  store.current = link;
48
- if (link) console.log("[timber:link-pending] setLink", {
49
- store,
50
- hasSetter: !!link.setIsPending
51
- });
52
48
  }
53
49
  /**
54
50
  * Unmount a link instance from navigation tracking.
@@ -296,4 +292,4 @@ function useSegmentParams(_route) {
296
292
  //#endregion
297
293
  export { getNavigationState as a, usePendingNavigationUrl as c, unmountLinkForCurrentNavigation as d, getRouter as f, NavigationProvider as i, LINK_IDLE as l, setGlobalRouter as m, useSegmentParams as n, setNavigationState as o, getRouterOrNull as p, setHardNavigating as r, useNavigationContext as s, setCurrentParams as t, setLinkForCurrentNavigation as u };
298
294
 
299
- //# sourceMappingURL=use-params-DrjaGSER.js.map
295
+ //# sourceMappingURL=use-params-Br9YSUFV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-params-Br9YSUFV.js","names":[],"sources":["../../src/client/router-ref.ts","../../src/client/link-pending-store.ts","../../src/client/navigation-context.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx","../../src/client/use-params.ts"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router.js';\nimport { globalRouter, _setGlobalRouter } from './state.js';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","/**\n * Link Pending Store — per-link optimistic pending state.\n *\n * Tracks which link instance is currently navigating so that only the\n * clicked link shows pending. Uses `useOptimistic` from React 19 —\n * the optimistic value (isPending=true) is set inside NavigationRoot's\n * async startTransition so it persists for the duration of the RSC\n * fetch, then auto-reverts to idle when the transition settles.\n *\n * Flow:\n * 1. Link click → setLinkForCurrentNavigation(instance) stores the ref\n * 2. NavigationRoot startTransition → activateLinkPending() calls\n * instance.setIsPending(LINK_PENDING) inside the async transition\n * 3. Transition settles → useOptimistic auto-reverts to LINK_IDLE\n *\n * SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys because\n * the RSC client bundler can duplicate this module across chunks.\n *\n * See design/19-client-navigation.md §\"Per-Link Pending State\"\n */\n\nimport type { LinkStatus } from './use-link-status.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface LinkPendingInstance {\n setIsPending: (status: LinkStatus) => void;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nconst LINK_PENDING_KEY = Symbol.for('__timber_link_pending');\n\n/** Status object: link navigation in flight */\nexport const LINK_PENDING: LinkStatus = { isPending: true };\n\n/** Status object: link idle */\nexport const LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Singleton Storage ───────────────────────────────────────────\n\nfunction getStore(): { current: LinkPendingInstance | null } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[LINK_PENDING_KEY]) {\n g[LINK_PENDING_KEY] = { current: null };\n }\n return g[LINK_PENDING_KEY] as { current: LinkPendingInstance | null };\n}\n\n// ─── Public API ──────────────────────────────────────────────────\n\n/**\n * Register the link instance that initiated the current navigation.\n * Called from Link's click handler. Does NOT call setIsPending —\n * that happens inside NavigationRoot's startTransition via\n * activateLinkPending().\n *\n * Resets the previous link to idle immediately (the old link's\n * useOptimistic reverts since its transition already settled).\n */\nexport function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {\n const store = getStore();\n store.current = link;\n}\n\n/**\n * Activate pending state on the current link instance.\n * MUST be called inside NavigationRoot's async startTransition —\n * this is what makes useOptimistic persist for the navigation duration.\n */\nexport function activateLinkPending(): void {\n const store = getStore();\n store.current?.setIsPending(LINK_PENDING);\n}\n\n/**\n * Reset the current link's pending state. With useOptimistic this is\n * handled automatically when the transition settles. Kept for callers\n * that explicitly need to clear on error paths.\n */\nexport function resetLinkPending(): void {\n const store = getStore();\n if (store.current) {\n store.current.setIsPending(LINK_IDLE);\n store.current = null;\n }\n}\n\n/**\n * @deprecated No longer needed with useOptimistic. Kept for callers.\n */\nexport function getCurrentNavId(): number {\n return 0;\n}\n\n/**\n * Clean up the link pending store entirely.\n */\nexport function clearLinkPendingSetter(): void {\n getStore().current = null;\n}\n\n/**\n * Unmount a link instance from navigation tracking.\n */\nexport function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void {\n const store = getStore();\n if (store.current === link) {\n store.current = null;\n }\n}\n","'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useSegmentParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\n *\n * SINGLETON GUARANTEE: All shared mutable state uses globalThis via\n * Symbol.for keys. The RSC client bundler can duplicate this module\n * across chunks (browser-entry graph + client-reference graph). With\n * ESM output, each chunk gets its own module scope — module-level\n * variables would create separate singleton instances per chunk.\n * globalThis guarantees a single instance regardless of duplication.\n *\n * This workaround will be removed when Rolldown ships `format: 'app'`\n * (module registry format that deduplicates like webpack/Turbopack).\n * See design/27-chunking-strategy.md.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\n *\n * Context instances are stored on globalThis (NOT in module-level\n * variables) because the ESM bundler can duplicate this module across\n * chunks. Module-level variables would create separate instances per\n * chunk — the provider in NavigationRoot (index chunk) would use\n * context A while the consumer in usePendingNavigation (shared chunk)\n * reads from context B. globalThis guarantees a single instance.\n *\n * See design/27-chunking-strategy.md §\"Singleton Safety\"\n *\n * NOTE: Despite similar naming, `usePendingNavigationUrl()` here is an\n * internal helper — the public hook is `usePendingNavigation()` in\n * use-pending-navigation.ts.\n */\n\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\nconst PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | React.Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useSegmentParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({\n value,\n children,\n}: NavigationProviderProps): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation state communicated between the router and renderRoot.\n *\n * The router calls setNavigationState() before renderRoot(). The\n * renderRoot callback reads via getNavigationState() to create the\n * NavigationProvider with the correct params/pathname.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n *\n * Stored on globalThis (like the context instances above) because the\n * router lives in one chunk while renderRoot lives in another. Module-\n * level variables would be separate per chunk.\n */\nconst NAV_STATE_KEY = Symbol.for('__timber_nav_state');\n\nfunction _getNavStateStore(): { current: NavigationState } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_STATE_KEY]) {\n g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/' } };\n }\n return g[NAV_STATE_KEY] as { current: NavigationState };\n}\n\nexport function setNavigationState(state: NavigationState): void {\n _getNavStateStore().current = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _getNavStateStore().current;\n}\n\n// ---------------------------------------------------------------------------\n// Pending Navigation Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * NavigationRoot (urgent useState), consumed by usePendingNavigation\n * and TopLoader. Per-link pending state uses useOptimistic instead\n * (see link-pending-store.ts).\n *\n * Uses globalThis via Symbol.for for the same reason as NavigationContext\n * above — the bundler may duplicate this module across chunks, and module-\n * level variables would create separate context instances.\n */\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as\n | React.Context<string | null>\n | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<string | null>(null);\n (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Navigation API transition state (optional progressive enhancement)\n// ---------------------------------------------------------------------------\n\n/**\n * Check if the browser's Navigation API has an active transition.\n *\n * When the Navigation API is available and a navigation has been intercepted\n * via event.intercept(), `navigation.transition` is non-null until the\n * handler resolves. This provides browser-native progress tracking that\n * can be used alongside the existing pendingUrl mechanism.\n *\n * Returns false when Navigation API is unavailable or no transition is active.\n */\nexport function hasNativeNavigationTransition(): boolean {\n if (typeof window === 'undefined') return false;\n const nav = (window as unknown as { navigation?: { transition?: unknown } }).navigation;\n return nav?.transition != null;\n}\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigationUrl, hasNativeNavigationTransition } from './navigation-context.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from PendingNavigationContext.\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n const pendingUrl = usePendingNavigationUrl();\n // Navigation is pending when either:\n // 1. Our React-based pending URL is set (standard path), OR\n // 2. The Navigation API has an active transition (external navigations\n // intercepted by the navigate event that haven't completed yet).\n // In practice these are almost always in sync — the Navigation API\n // transition is active while our pendingUrl is set. This check ensures\n // the top-loader also shows for external navigations caught by the\n // Navigation API before our React state updates.\n const isPending = pendingUrl !== null || hasNativeNavigationTransition();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — Wrapper component for transition-based rendering.\n *\n * Solves the \"new boundary has no old content\" problem for client-side\n * navigation. When React renders a completely new Suspense boundary via\n * root.render(), it shows the fallback immediately — root.render() is\n * always an urgent update regardless of startTransition.\n *\n * NavigationRoot holds the current element in React state. Navigation\n * updates call startTransition(() => setState(newElement)), which IS\n * a transition update. React keeps the old committed tree visible while\n * any new Suspense boundaries in the transition resolve.\n *\n * Also manages `pendingUrl` as React state with an urgent/transition split:\n * - Navigation START: `setPendingUrl(url)` is an urgent update — React\n * commits it before the next paint, showing the spinner immediately.\n * - Navigation END: `setPendingUrl(null)` is inside `startTransition`\n * alongside `setElement(newTree)` — both commit atomically, so the\n * spinner disappears in the same frame as the new content appears.\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, startTransition, useState, type ReactNode } from 'react';\nimport { activateLinkPending, resetLinkPending } from './link-pending-store.js';\nimport { PendingNavigationProvider } from './navigation-context.js';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.js';\n\n// ─── Navigation Transition Counter ──────────────────────────────\n// Monotonically increasing counter that increments each time\n// navigateTransition() is called. Used to detect stale transitions:\n// if a newer transition started while the current one's perform()\n// was in flight, the current transition is stale and should reject.\n//\n// Separate from the link-pending navId (which only increments on\n// link clicks). This counter covers all navigation types: link clicks,\n// programmatic navigate(), refresh(), and handlePopState().\n//\n// Uses globalThis for singleton guarantee across chunks — same pattern\n// as NavigationContext and the link pending store.\n\nconst NAV_TRANSITION_KEY = Symbol.for('__timber_nav_transition_counter');\n\nfunction getTransitionCounter(): { id: number } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_TRANSITION_KEY]) {\n g[NAV_TRANSITION_KEY] = { id: 0 };\n }\n return g[NAV_TRANSITION_KEY] as { id: number };\n}\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Module-level functions ──────────────────────────────────────\n\n/**\n * Module-level reference to the state setter wrapped in startTransition.\n * Used for non-navigation renders (applyRevalidation, popstate replay).\n */\nlet _transitionRender: ((element: ReactNode) => void) | null = null;\n\n/**\n * Module-level reference to the navigation transition function.\n * Wraps a full navigation (fetch + render) in a single startTransition\n * with the pending URL.\n */\nlet _navigateTransition:\n | ((pendingUrl: string, perform: () => Promise<ReactNode>) => Promise<void>)\n | null = null;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root wrapper component that enables transition-based rendering.\n *\n * Renders PendingNavigationProvider around children for the pending URL\n * context. The DOM tree matches the server-rendered HTML during hydration\n * (the provider renders no extra DOM elements).\n *\n * Usage in browser-entry.ts:\n * const rootEl = createElement(NavigationRoot, { initial: wrapped });\n * reactRoot = hydrateRoot(document, rootEl);\n *\n * Subsequent navigations:\n * navigateTransition(url, async () => { fetch; return wrappedElement; });\n *\n * Non-navigation renders:\n * transitionRender(newWrappedElement);\n */\nexport function NavigationRoot({\n initial,\n topLoaderConfig,\n}: {\n initial: ReactNode;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n const [element, setElement] = useState<ReactNode>(initial);\n const [pendingUrl, setPendingUrl] = useState<string | null>(null);\n\n // NOTE: We use standalone `startTransition` (imported from 'react'),\n // NOT `useTransition`. The `useTransition` hook's `startTransition`\n // is tied to a single fiber and tracks one async callback at a time.\n // When two navigations overlap (click slow-page, then click dashboard),\n // calling useTransition's startTransition twice with concurrent async\n // callbacks corrupts React's internal hook tracking — causing\n // \"Rendered more hooks than during the previous render.\"\n //\n // Standalone `startTransition` creates independent transition lanes\n // for each call, so concurrent navigations don't interfere. We don't\n // need useTransition's `isPending` — we track pending state via our\n // own `pendingUrl` useState.\n //\n // This matches the Next.js pattern (TIM-625): \"No useTransition in\n // the router at all — only standalone startTransition.\"\n\n // Non-navigation render (revalidation, popstate cached replay).\n _transitionRender = (newElement: ReactNode) => {\n startTransition(() => {\n setElement(newElement);\n });\n };\n\n // Full navigation transition.\n // setPendingUrl(url) is an URGENT update — React commits it before the next\n // paint, so the pending spinner appears immediately when navigation starts.\n // Inside startTransition: the async fetch + setElement + setPendingUrl(null)\n // are deferred. When the transition commits, the new tree and pendingUrl=null\n // both apply in the same React commit — making the pending→active transition\n // atomic (no frame where pending is false but the old tree is still visible).\n _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {\n // Urgent: show pending state immediately (for TopLoader / usePendingNavigation)\n setPendingUrl(url);\n\n // Increment the transition counter SYNCHRONOUSLY (before startTransition\n // schedules the async work). Each call gets a unique transId; the counter\n // is the same globalThis singleton, so a newer call always has a higher id.\n const counter = getTransitionCounter();\n const transId = ++counter.id;\n\n return new Promise<void>((resolve, reject) => {\n startTransition(async () => {\n // Activate per-link pending state inside this async transition.\n // useOptimistic persists the isPending=true value for the duration\n // of this transition, then auto-reverts when it settles.\n activateLinkPending();\n try {\n const newElement = await perform();\n // Only commit state if this is still the active navigation.\n // A superseded transition's updates must be dropped entirely.\n if (counter.id === transId) {\n setElement(newElement);\n setPendingUrl(null);\n resolve();\n } else {\n // Stale transition — a newer navigation has superseded this one.\n // Reject so the caller (navigate/refresh/handlePopState) doesn't\n // run post-transition side effects (applyHead, scroll, event\n // dispatch) with stale data. All callers catch AbortError.\n reject(new DOMException('Navigation superseded', 'AbortError'));\n }\n } catch (err) {\n // Only clear pending if this is still the active navigation.\n if (counter.id === transId) {\n setPendingUrl(null);\n resetLinkPending();\n }\n reject(err);\n }\n });\n });\n };\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the two\n // useState calls above). React requires the same hooks to run on\n // every render. If we threw before hooks, React would see 0 hooks\n // on the re-render vs 2 hooks on the initial render — triggering\n // the exact \"Rendered more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree inside PendingNavigationProvider.\n // The TopLoader reads pendingUrl from context to show/hide the progress bar.\n // It is rendered only when not explicitly disabled via config.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n const children = showTopLoader\n ? createElement(Fragment, null, createElement(TopLoader, { config: topLoaderConfig }), element)\n : element;\n return createElement(PendingNavigationProvider, { value: pendingUrl }, children);\n}\n\n// ─── Public API ──────────────────────────────────────────────────\n\n/**\n * Trigger a transition render for non-navigation updates.\n * React keeps the old committed tree visible while any new Suspense\n * boundaries in the update resolve.\n *\n * Used for: applyRevalidation, popstate replay with cached payload.\n */\nexport function transitionRender(element: ReactNode): void {\n if (_transitionRender) {\n _transitionRender(element);\n }\n}\n\n/**\n * Run a full navigation inside a React transition with optimistic pending URL.\n *\n * The `perform` callback runs inside `startTransition` — it should fetch the\n * RSC payload, update router state, and return the wrapped React element.\n * The pending URL shows immediately (urgent update) and reverts\n * to null when the transition commits (atomic with the new tree).\n *\n * Returns a Promise that resolves when the async work completes (note: the\n * React transition may not have committed yet, but all state updates are done).\n *\n * Used for: navigate(), refresh(), popstate with fetch.\n */\nexport function navigateTransition(\n pendingUrl: string,\n perform: () => Promise<ReactNode>\n): Promise<void> {\n if (_navigateTransition) {\n return _navigateTransition(pendingUrl, perform);\n }\n // Fallback: no NavigationRoot mounted (shouldn't happen in production)\n return perform().then(() => {});\n}\n\n/**\n * Check if the NavigationRoot is mounted and ready for renders.\n * Used by browser-entry.ts to guard against renders before hydration.\n */\nexport function isNavigationRootReady(): boolean {\n return _transitionRender !== null;\n}\n\n/**\n * Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).\n *\n * When there's no RSC payload, we can't create a React root immediately —\n * `createRoot(document).render(...)` would blank the SSR HTML. Instead,\n * this sets up `_transitionRender` and `_navigateTransition` so that the\n * first client navigation triggers root creation via `createAndMount`.\n *\n * After `createAndMount` runs, NavigationRoot renders and overwrites these\n * callbacks with its real `startTransition`-based implementations.\n */\nexport function installDeferredNavigation(createAndMount: (initial: ReactNode) => void): void {\n let mounted = false;\n const mountOnce = (element: ReactNode) => {\n if (mounted) return;\n mounted = true;\n createAndMount(element);\n };\n _transitionRender = (element: ReactNode) => {\n mountOnce(element);\n };\n _navigateTransition = async (_pendingUrl: string, perform: () => Promise<ReactNode>) => {\n const element = await perform();\n mountOnce(element);\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 * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\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 * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known route 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 route: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(route?: string): Record<string, string | string[]>;\nexport function useSegmentParams(_route?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { n as useQueryStates } from "../_chunks/use-query-states-Lo_s_pw2.js";
5
5
  import { t as mergePreservedSearchParams } from "../_chunks/merge-search-params-Cm_KIWDX.js";
6
6
  import { c as cachedSearchParams, i as _setCachedSearch, n as getSsrData, s as cachedSearch } from "../_chunks/ssr-data-DzuI0bIV.js";
7
7
  import { n as useSegmentContext } from "../_chunks/segment-context-fHFLF1PE.js";
8
- import { c as usePendingNavigationUrl, d as unmountLinkForCurrentNavigation, l as LINK_IDLE, n as useSegmentParams, p as getRouterOrNull, s as useNavigationContext, u as setLinkForCurrentNavigation } from "../_chunks/use-params-DrjaGSER.js";
8
+ import { c as usePendingNavigationUrl, d as unmountLinkForCurrentNavigation, l as LINK_IDLE, n as useSegmentParams, p as getRouterOrNull, s as useNavigationContext, u as setLinkForCurrentNavigation } from "../_chunks/use-params-Br9YSUFV.js";
9
9
  import { t as _registerUseCookieModule } from "../_chunks/define-cookie-C2IkoFGN.js";
10
10
  import { createContext, useActionState as useActionState$1, useContext, useEffect, useOptimistic, useRef, useSyncExternalStore, useTransition } from "react";
11
11
  import { jsx } from "react/jsx-runtime";
@@ -243,10 +243,6 @@ var Link = function LinkImpl(props) {
243
243
  const router = getRouterOrNull();
244
244
  if (!router) return;
245
245
  const shouldScroll = scroll !== false;
246
- console.log("[timber:link] onClick — setting link for nav", {
247
- hasInstance: !!linkInstanceRef.current,
248
- hasNavigationApi: hasNavigationApi()
249
- });
250
246
  setLinkForCurrentNavigation(linkInstanceRef.current);
251
247
  if (hasNavigationApi()) {
252
248
  setNavLinkMetadata({
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/nav-link-store.ts","../../src/client/navigation-api.ts","../../src/client/link.tsx","../../src/client/use-pending-navigation.ts","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/use-search-params.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/use-cookie.ts","../../src/client/index.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","/**\n * Navigation Link Store — passes per-link metadata from Link's onClick\n * to the Navigation API's navigate event handler.\n *\n * When the Navigation API is active, Link does NOT call event.preventDefault()\n * or router.navigate(). Instead it stores metadata (scroll option, link\n * pending instance) here, and lets the <a> click propagate naturally.\n * The navigate event handler reads this metadata to configure the RSC\n * navigation with the correct options.\n *\n * This store is consumed once per navigation — after reading, the metadata\n * is cleared. If no metadata is present (e.g., a plain <a> tag without\n * our Link component), the navigate handler uses default options.\n *\n * See design/19-client-navigation.md §\"Navigation API Integration\"\n */\n\nimport type { LinkPendingInstance } from './link-pending-store.js';\n\nexport interface NavLinkMetadata {\n /** Whether to scroll to top after navigation. Default: true. */\n scroll: boolean;\n /** The Link's pending state instance for per-link status tracking. */\n linkInstance: LinkPendingInstance | null;\n}\n\nlet pendingMetadata: NavLinkMetadata | null = null;\n\n/**\n * Store metadata from Link's onClick for the next navigate event.\n * Called synchronously in the click handler — the navigate event\n * fires synchronously after onClick returns.\n */\nexport function setNavLinkMetadata(metadata: NavLinkMetadata): void {\n pendingMetadata = metadata;\n}\n\n/**\n * Consume the stored metadata. Returns null if no Link onClick\n * preceded this navigation (e.g., plain <a> tag, programmatic nav).\n * Clears the store after reading.\n */\nexport function consumeNavLinkMetadata(): NavLinkMetadata | null {\n const metadata = pendingMetadata;\n pendingMetadata = null;\n return metadata;\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 type { NavigationApi, NavigateEvent } from './navigation-api-types.js';\nimport { consumeNavLinkMetadata } from './nav-link-store.js';\nimport { isHardNavigating } from './navigation-root.js';\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 (\n typeof window !== 'undefined' &&\n 'navigation' in window &&\n (window as unknown as { navigation: unknown }).navigation != null\n );\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n * Uses type assertion — we never import Navigation API types unconditionally.\n */\nexport function getNavigationApi(): NavigationApi | null {\n if (!hasNavigationApi()) return null;\n return (window as unknown as { navigation: NavigationApi }).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, with metadata from\n * nav-link-store) and external 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 }\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/**\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 — aligned with when the TopLoader's\n * pendingUrl clears (same finally block in router.navigate).\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 — aligned with TopLoader's pendingUrl lifecycle.\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 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 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 + 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 — either a Link <a> click (with metadata in\n // nav-link-store) or an external navigation (plain <a>, programmatic).\n // Consume link metadata if present — tells us scroll preference\n // and which Link component to track pending state for.\n const linkMeta = consumeNavLinkMetadata();\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 event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onExternalNavigate(url, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: linkMeta?.scroll,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate as EventListener);\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 as EventListener);\n },\n };\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 serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useOptimistic,\n useEffect,\n useRef,\n type AnchorHTMLAttributes,\n type JSX,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { SearchParamsDefinition } from '../search-params/define.js';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';\nimport { LinkStatusContext } from './use-link-status.js';\nimport { getRouterOrNull } from './router-ref.js';\nimport { getSsrData } from './ssr-data.js';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nimport {\n setLinkForCurrentNavigation,\n unmountLinkForCurrentNavigation,\n LINK_IDLE,\n type LinkPendingInstance,\n} from './link-pending-store.js';\nimport { setNavLinkMetadata } from './nav-link-store.js';\nimport { hasNavigationApi } from './navigation-api.js';\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 from\n * the request context (getSsrData). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\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 */\ninterface 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/**\n * Href types accepted by the catch-all (non-route) call signature.\n *\n * - External protocols: https://, http://, mailto:, tel:, ftp://\n * - Hash-only and query-only links: #section, ?param=value\n * - Computed `string` variables (non-literal)\n *\n * Internal path literals like \"/typo-route\" do NOT match — they must\n * be a known route (from codegen) or stored in a `string` variable.\n * This catches wrong hrefs at the type level. See TIM-624.\n */\ntype ExternalHref =\n | `http://${string}`\n | `https://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `ftp://${string}`\n | `//${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * Callable interface for the Link component.\n *\n * Two kinds of call signatures:\n * 1. Per-route (added by codegen via interface merging): DIRECT types\n * for segmentParams — preserves TypeScript excess property checking.\n * 2. Catch-all (below): accepts external hrefs and computed `string`\n * variables. Does NOT accept internal path literals — those must\n * match a known route from the codegen.\n *\n * See TIM-624.\n */\nexport interface LinkFunction {\n // External links (literal protocol hrefs)\n (\n props: LinkBaseProps & {\n href: ExternalHref;\n segmentParams?: never;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n ): JSX.Element;\n // Computed/variable href (non-literal string) — e.g. href={myVar}\n // `string extends H` is true only when H is the wide `string` type,\n // not a specific literal. Template literal hrefs like `/blog/${slug}`\n // are handled by resolved-pattern signatures in the codegen.\n <H extends string>(\n props: string extends H\n ? LinkBaseProps & {\n href: H;\n segmentParams?: Record<string, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n : never\n ): JSX.Element;\n}\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, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\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?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, string | number | string[]>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\n// ─── Dangerous URL Scheme Detection ──────────────────────────────\n\n/**\n * Reject dangerous URL schemes that could execute script.\n * Security: design/13-security.md § Link scheme injection (test #9)\n */\nconst DANGEROUS_SCHEMES = /^\\s*(javascript|data|vbscript):/i;\n\nexport function validateLinkHref(href: string): void {\n if (DANGEROUS_SCHEMES.test(href)) {\n throw new Error(\n `<Link> received a dangerous href: \"${href}\". ` +\n 'javascript:, data:, and vbscript: URLs are not allowed.'\n );\n }\n}\n\n// ─── Internal Link Detection ─────────────────────────────────────\n\n/** Returns true if the href is an internal path (not an external URL) */\nexport function isInternalHref(href: string): boolean {\n // Relative paths, root-relative paths, and hash links are internal\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n // Anything with a protocol scheme is external\n if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {\n return false;\n }\n // Bare relative paths (e.g., \"dashboard\") are internal\n return true;\n}\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 */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, string | number | string[]>,\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 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 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 return encodeURIComponent(String(value));\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(\n pattern: string,\n params: Record<string, string | number | string[]>\n): 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 via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\nexport function resolveHref(\n href: string,\n params?: Record<string, string | number | string[]>,\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n }\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 const qs = searchParams.definition.serialize(searchParams.values);\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, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\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 (useOptimistic) ────────────────────────\n // Each Link has its own pending state. Only the clicked link's\n // setter is invoked during navigation — zero other links re-render.\n //\n // Link click stores the instance; NavigationRoot activates inside startTransition.\n // useOptimistic auto-reverts to LINK_IDLE when the navigation\n // startTransition — batched with the new tree commit.\n //\n // See design/19-client-navigation.md §\"Per-Link Pending State\"\n const [linkStatus, setIsPending] = useOptimistic(LINK_IDLE);\n\n // Build the link instance ref for the pending store.\n // The ref is stable across renders — we update the setter on each\n // render to keep it current.\n const linkInstanceRef = useRef<LinkPendingInstance | null>(null);\n if (!linkInstanceRef.current) {\n linkInstanceRef.current = { setIsPending };\n } else {\n linkInstanceRef.current.setIsPending = setIsPending;\n }\n\n // Clean up if this link unmounts while it's the current navigation link.\n // Prevents calling setOptimistic on an unmounted component.\n useEffect(() => {\n const instance = linkInstanceRef.current;\n return () => {\n if (instance) {\n unmountLinkForCurrentNavigation(instance);\n }\n };\n }, []);\n\n // Preserve search params from the current URL when requested.\n // useSearchParams() works during both SSR (reads from request context)\n // and on the client (reads from window.location, reactive to URL changes).\n // We read current search params directly to avoid unconditional hook calls.\n // On the client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's search params.\n const resolvedHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n const internal = isInternalHref(resolvedHref);\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 // 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; // SSR or pre-hydration — fall through to browser nav\n\n const shouldScroll = scroll !== false;\n\n // Register this link in the pending store. The actual\n // setIsPending(LINK_PENDING) call happens inside NavigationRoot's\n // async startTransition via activateLinkPending().\n\n console.log('[timber:link] onClick — setting link for nav', { hasInstance: !!linkInstanceRef.current, hasNavigationApi: hasNavigationApi() });\n setLinkForCurrentNavigation(linkInstanceRef.current);\n\n // When Navigation API is active, let the <a> click propagate\n // naturally — do NOT call preventDefault(). The navigate event\n // handler intercepts it and runs the RSC pipeline. This is a\n // user-initiated navigation, so Chrome shows the native loading\n // indicator (tab spinner). Metadata (scroll, link instance) is\n // passed via nav-link-store so the handler can configure the nav.\n //\n // Without Navigation API (fallback), preventDefault and drive\n // navigation through the router as before.\n if (hasNavigationApi()) {\n setNavLinkMetadata({\n scroll: shouldScroll,\n linkInstance: linkInstanceRef.current,\n });\n // Don't preventDefault — let the <a> click fire the navigate event\n return;\n }\n\n // History API fallback — prevent default and navigate via router\n event.preventDefault();\n\n // Re-merge preserved search params at click time to pick up any\n // URL changes since render (e.g. from other navigations or pushState).\n const navHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n\n void router.navigate(navHref, { scroll: shouldScroll });\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 // Re-merge preserved search params at hover time for fresh prefetch URL\n const prefetchHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n router.prefetch(prefetchHref);\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","// usePendingNavigation — returns true while an RSC navigation is in flight.\n// See design/19-client-navigation.md §\"usePendingNavigation()\"\n//\n// Reads from PendingNavigationContext (provided by NavigationRoot) so the\n// pending state shows immediately (urgent update) and clears atomically\n// with the new tree (same startTransition commit).\n\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * The pending state is true from the moment the RSC fetch starts until\n * React reconciliation completes. This includes the fetch itself,\n * RSC stream parsing, and React tree reconciliation.\n *\n * It does NOT include Suspense streaming after the shell — only the\n * initial shell reconciliation.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n const pendingUrl = usePendingNavigationUrl();\n // During SSR or outside PendingNavigationProvider, no navigation is pending\n return pendingUrl !== null;\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 (params, pathname) 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.js';\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 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 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 * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * This is a thin wrapper over window.location.search, provided for\n * Next.js API compatibility (libraries like nuqs import useSearchParams\n * from next/navigation).\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction getServerSearch(): string {\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction subscribe(callback: () => void): () => void {\n window.addEventListener('popstate', callback);\n return () => window.removeEventListener('popstate', callback);\n}\n\n// Cache the last search string and its parsed URLSearchParams to avoid\n// creating a new object on every render when the URL hasn't changed.\n// State lives in client/state.ts for singleton guarantees.\n\nfunction getSearchParams(): URLSearchParams {\n const search = getSearch();\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\nfunction getServerSearchParams(): URLSearchParams {\n const data = getSsrData();\n return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n // useSyncExternalStore needs a primitive snapshot for comparison.\n // We use the raw search string as the snapshot, then return the\n // parsed URLSearchParams.\n useSyncExternalStore(subscribe, getSearch, getServerSearch);\n return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();\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.js';\nimport { usePathname } from './use-pathname.js';\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 { ActionResult, ValidationErrors } from '../server/action-client';\nimport type { FormFlashData } from '../server/form-flash';\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 — matches React 19's useActionState return.\n * [result, formAction, isPending]\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\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 return reactUseActionState(action, initialState as ActionResult<TData> | null, permalink);\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>(\n action: (input: unknown) => Promise<ActionResult<TData>>\n): [(input?: unknown) => Promise<ActionResult<TData>>, boolean] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: unknown): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await action(input);\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── useFormErrors ──────────────────────────────────────────────────────\n\n/** Return type of useFormErrors(). */\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 * Extract per-field and form-level errors from an ActionResult.\n *\n * Pure function (no internal hooks) — follows React naming convention\n * since it's used in render. Accepts the result from `useActionState`\n * or flash data from `getFormFlash()`.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = useFormErrors(result)\n *\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}\n * {errors.formErrors.map(e => <p key={e}>{e}</p>)}\n * </form>\n * )\n * ```\n */\nexport function useFormErrors<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 * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nfunction getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nfunction serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n","// @timber-js/app/client — Client-side primitives\n// These are the primary imports for client components.\n//\n// Framework-internal bootstrap, SSR bridge, and segment plumbing is in\n// #client-internal (Node package import).\n// Design doc: design/triage/api-naming-spike.md §8.5\n\n// JsonSerializable moved to @timber-js/app/codec as the single canonical path.\n// Re-export removed per TIM-721.\nexport type { RenderErrorDigest } from './types';\n\n// Navigation\nexport { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';\nexport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nexport type { LinkFunction, LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';\nexport type { LinkSegmentParams, OnNavigateHandler, OnNavigateEvent } from './link';\nexport { usePendingNavigation } from './use-pending-navigation';\nexport { useLinkStatus, LinkStatusContext } from './use-link-status';\nexport type { LinkStatus } from './use-link-status';\nexport { useRouter } from './use-router';\nexport type { AppRouterInstance } from './use-router';\nexport { usePathname } from './use-pathname';\nexport { useSearchParams } from './use-search-params';\nexport { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selected-layout-segment';\n\n// Forms\nexport { useActionState, useFormAction, useFormErrors } from './form';\nexport type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';\n\n// Params\nexport { useSegmentParams } from './use-params';\n\n// Query states (URL-synced search params)\nexport { useQueryStates } from './use-query-states';\n\n// Cookies\nexport { useCookie } from './use-cookie';\nexport type { ClientCookieOptions, CookieSetter } from './use-cookie';\n\n// Register the client cookie module with defineCookie's lazy reference.\n// This runs at module load time in the client/SSR environment, wiring up\n// the useCookie hook without a top-level import in define-cookie.ts.\nimport * as _useCookieMod from './use-cookie.js';\nimport { _registerUseCookieModule } from '../cookies/define-cookie.js';\n_registerUseCookieModule(_useCookieMod);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/nav-link-store.ts","../../src/client/navigation-api.ts","../../src/client/link.tsx","../../src/client/use-pending-navigation.ts","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/use-search-params.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/use-cookie.ts","../../src/client/index.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","/**\n * Navigation Link Store — passes per-link metadata from Link's onClick\n * to the Navigation API's navigate event handler.\n *\n * When the Navigation API is active, Link does NOT call event.preventDefault()\n * or router.navigate(). Instead it stores metadata (scroll option, link\n * pending instance) here, and lets the <a> click propagate naturally.\n * The navigate event handler reads this metadata to configure the RSC\n * navigation with the correct options.\n *\n * This store is consumed once per navigation — after reading, the metadata\n * is cleared. If no metadata is present (e.g., a plain <a> tag without\n * our Link component), the navigate handler uses default options.\n *\n * See design/19-client-navigation.md §\"Navigation API Integration\"\n */\n\nimport type { LinkPendingInstance } from './link-pending-store.js';\n\nexport interface NavLinkMetadata {\n /** Whether to scroll to top after navigation. Default: true. */\n scroll: boolean;\n /** The Link's pending state instance for per-link status tracking. */\n linkInstance: LinkPendingInstance | null;\n}\n\nlet pendingMetadata: NavLinkMetadata | null = null;\n\n/**\n * Store metadata from Link's onClick for the next navigate event.\n * Called synchronously in the click handler — the navigate event\n * fires synchronously after onClick returns.\n */\nexport function setNavLinkMetadata(metadata: NavLinkMetadata): void {\n pendingMetadata = metadata;\n}\n\n/**\n * Consume the stored metadata. Returns null if no Link onClick\n * preceded this navigation (e.g., plain <a> tag, programmatic nav).\n * Clears the store after reading.\n */\nexport function consumeNavLinkMetadata(): NavLinkMetadata | null {\n const metadata = pendingMetadata;\n pendingMetadata = null;\n return metadata;\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 type { NavigationApi, NavigateEvent } from './navigation-api-types.js';\nimport { consumeNavLinkMetadata } from './nav-link-store.js';\nimport { isHardNavigating } from './navigation-root.js';\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 (\n typeof window !== 'undefined' &&\n 'navigation' in window &&\n (window as unknown as { navigation: unknown }).navigation != null\n );\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n * Uses type assertion — we never import Navigation API types unconditionally.\n */\nexport function getNavigationApi(): NavigationApi | null {\n if (!hasNavigationApi()) return null;\n return (window as unknown as { navigation: NavigationApi }).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, with metadata from\n * nav-link-store) and external 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 }\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/**\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 — aligned with when the TopLoader's\n * pendingUrl clears (same finally block in router.navigate).\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 — aligned with TopLoader's pendingUrl lifecycle.\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 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 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 + 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 — either a Link <a> click (with metadata in\n // nav-link-store) or an external navigation (plain <a>, programmatic).\n // Consume link metadata if present — tells us scroll preference\n // and which Link component to track pending state for.\n const linkMeta = consumeNavLinkMetadata();\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 event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onExternalNavigate(url, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: linkMeta?.scroll,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate as EventListener);\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 as EventListener);\n },\n };\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 serialized via SearchParamsDefinition\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useOptimistic,\n useEffect,\n useRef,\n type AnchorHTMLAttributes,\n type JSX,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { SearchParamsDefinition } from '../search-params/define.js';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';\nimport { LinkStatusContext } from './use-link-status.js';\nimport { getRouterOrNull } from './router-ref.js';\nimport { getSsrData } from './ssr-data.js';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nimport {\n setLinkForCurrentNavigation,\n unmountLinkForCurrentNavigation,\n LINK_IDLE,\n type LinkPendingInstance,\n} from './link-pending-store.js';\nimport { setNavLinkMetadata } from './nav-link-store.js';\nimport { hasNavigationApi } from './navigation-api.js';\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 from\n * the request context (getSsrData). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\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 */\ninterface 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/**\n * Href types accepted by the catch-all (non-route) call signature.\n *\n * - External protocols: https://, http://, mailto:, tel:, ftp://\n * - Hash-only and query-only links: #section, ?param=value\n * - Computed `string` variables (non-literal)\n *\n * Internal path literals like \"/typo-route\" do NOT match — they must\n * be a known route (from codegen) or stored in a `string` variable.\n * This catches wrong hrefs at the type level. See TIM-624.\n */\ntype ExternalHref =\n | `http://${string}`\n | `https://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `ftp://${string}`\n | `//${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * Callable interface for the Link component.\n *\n * Two kinds of call signatures:\n * 1. Per-route (added by codegen via interface merging): DIRECT types\n * for segmentParams — preserves TypeScript excess property checking.\n * 2. Catch-all (below): accepts external hrefs and computed `string`\n * variables. Does NOT accept internal path literals — those must\n * match a known route from the codegen.\n *\n * See TIM-624.\n */\nexport interface LinkFunction {\n // External links (literal protocol hrefs)\n (\n props: LinkBaseProps & {\n href: ExternalHref;\n segmentParams?: never;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n ): JSX.Element;\n // Computed/variable href (non-literal string) — e.g. href={myVar}\n // `string extends H` is true only when H is the wide `string` type,\n // not a specific literal. Template literal hrefs like `/blog/${slug}`\n // are handled by resolved-pattern signatures in the codegen.\n <H extends string>(\n props: string extends H\n ? LinkBaseProps & {\n href: H;\n segmentParams?: Record<string, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n }\n : never\n ): JSX.Element;\n}\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, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\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?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, string | number | string[]>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\n// ─── Dangerous URL Scheme Detection ──────────────────────────────\n\n/**\n * Reject dangerous URL schemes that could execute script.\n * Security: design/13-security.md § Link scheme injection (test #9)\n */\nconst DANGEROUS_SCHEMES = /^\\s*(javascript|data|vbscript):/i;\n\nexport function validateLinkHref(href: string): void {\n if (DANGEROUS_SCHEMES.test(href)) {\n throw new Error(\n `<Link> received a dangerous href: \"${href}\". ` +\n 'javascript:, data:, and vbscript: URLs are not allowed.'\n );\n }\n}\n\n// ─── Internal Link Detection ─────────────────────────────────────\n\n/** Returns true if the href is an internal path (not an external URL) */\nexport function isInternalHref(href: string): boolean {\n // Relative paths, root-relative paths, and hash links are internal\n if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {\n return true;\n }\n // Anything with a protocol scheme is external\n if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {\n return false;\n }\n // Bare relative paths (e.g., \"dashboard\") are internal\n return true;\n}\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 */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, string | number | string[]>,\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 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 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 return encodeURIComponent(String(value));\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(\n pattern: string,\n params: Record<string, string | number | string[]>\n): 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 via SearchParamsDefinition\n * - Validation that searchParams and inline query strings are exclusive\n */\nexport function resolveHref(\n href: string,\n params?: Record<string, string | number | string[]>,\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n }\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 const qs = searchParams.definition.serialize(searchParams.values);\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, string | number | string[]>;\n searchParams?: {\n definition: SearchParamsDefinition<Record<string, unknown>>;\n values: Record<string, unknown>;\n };\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 (useOptimistic) ────────────────────────\n // Each Link has its own pending state. Only the clicked link's\n // setter is invoked during navigation — zero other links re-render.\n //\n // Link click stores the instance; NavigationRoot activates inside startTransition.\n // useOptimistic auto-reverts to LINK_IDLE when the navigation\n // startTransition — batched with the new tree commit.\n //\n // See design/19-client-navigation.md §\"Per-Link Pending State\"\n const [linkStatus, setIsPending] = useOptimistic(LINK_IDLE);\n\n // Build the link instance ref for the pending store.\n // The ref is stable across renders — we update the setter on each\n // render to keep it current.\n const linkInstanceRef = useRef<LinkPendingInstance | null>(null);\n if (!linkInstanceRef.current) {\n linkInstanceRef.current = { setIsPending };\n } else {\n linkInstanceRef.current.setIsPending = setIsPending;\n }\n\n // Clean up if this link unmounts while it's the current navigation link.\n // Prevents calling setOptimistic on an unmounted component.\n useEffect(() => {\n const instance = linkInstanceRef.current;\n return () => {\n if (instance) {\n unmountLinkForCurrentNavigation(instance);\n }\n };\n }, []);\n\n // Preserve search params from the current URL when requested.\n // useSearchParams() works during both SSR (reads from request context)\n // and on the client (reads from window.location, reactive to URL changes).\n // We read current search params directly to avoid unconditional hook calls.\n // On the client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's search params.\n const resolvedHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n const internal = isInternalHref(resolvedHref);\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 // 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; // SSR or pre-hydration — fall through to browser nav\n\n const shouldScroll = scroll !== false;\n\n // Register this link in the pending store. The actual\n // setIsPending(LINK_PENDING) call happens inside NavigationRoot's\n // async startTransition via activateLinkPending().\n\n setLinkForCurrentNavigation(linkInstanceRef.current);\n\n // When Navigation API is active, let the <a> click propagate\n // naturally — do NOT call preventDefault(). The navigate event\n // handler intercepts it and runs the RSC pipeline. This is a\n // user-initiated navigation, so Chrome shows the native loading\n // indicator (tab spinner). Metadata (scroll, link instance) is\n // passed via nav-link-store so the handler can configure the nav.\n //\n // Without Navigation API (fallback), preventDefault and drive\n // navigation through the router as before.\n if (hasNavigationApi()) {\n setNavLinkMetadata({\n scroll: shouldScroll,\n linkInstance: linkInstanceRef.current,\n });\n // Don't preventDefault — let the <a> click fire the navigate event\n return;\n }\n\n // History API fallback — prevent default and navigate via router\n event.preventDefault();\n\n // Re-merge preserved search params at click time to pick up any\n // URL changes since render (e.g. from other navigations or pushState).\n const navHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n\n void router.navigate(navHref, { scroll: shouldScroll });\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 // Re-merge preserved search params at hover time for fresh prefetch URL\n const prefetchHref = preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref;\n router.prefetch(prefetchHref);\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","// usePendingNavigation — returns true while an RSC navigation is in flight.\n// See design/19-client-navigation.md §\"usePendingNavigation()\"\n//\n// Reads from PendingNavigationContext (provided by NavigationRoot) so the\n// pending state shows immediately (urgent update) and clears atomically\n// with the new tree (same startTransition commit).\n\nimport { usePendingNavigationUrl } from './navigation-context.js';\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * The pending state is true from the moment the RSC fetch starts until\n * React reconciliation completes. This includes the fetch itself,\n * RSC stream parsing, and React tree reconciliation.\n *\n * It does NOT include Suspense streaming after the shell — only the\n * initial shell reconciliation.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n const pendingUrl = usePendingNavigationUrl();\n // During SSR or outside PendingNavigationProvider, no navigation is pending\n return pendingUrl !== null;\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 (params, pathname) 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.js';\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 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 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 * On the client, reads from NavigationContext which is updated atomically\n * with the RSC tree render. This replaces the previous useSyncExternalStore\n * approach which only subscribed to popstate events — meaning usePathname()\n * did NOT re-render on forward navigation (pushState). The context approach\n * fixes this: pathname updates in the same render pass as the new tree.\n *\n * During SSR, reads the request pathname from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { getSsrData } from './ssr-data.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n/**\n * Read the current URL pathname.\n *\n * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). During SSR, reads from the\n * ALS-backed SSR data context. Falls back to window.location.pathname\n * when called outside a React component (e.g., in tests).\n */\nexport function usePathname(): string {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.pathname;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to SSR/fallback below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n const ssrData = getSsrData();\n if (ssrData) return ssrData.pathname ?? '/';\n\n // Final fallback: window.location (tests, edge cases).\n if (typeof window !== 'undefined') return window.location.pathname;\n return '/';\n}\n","/**\n * useSearchParams() — client-side hook for reading URL search params.\n *\n * Returns a read-only URLSearchParams instance reflecting the current\n * URL's query string. Updates when client-side navigation changes the URL.\n *\n * This is a thin wrapper over window.location.search, provided for\n * Next.js API compatibility (libraries like nuqs import useSearchParams\n * from next/navigation).\n *\n * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard\n * URLSearchParams. Mutation methods (set, delete, append) work on the\n * local copy but do NOT affect the URL — use the router or nuqs for that.\n *\n * During SSR, reads the request search params from the SSR ALS context\n * (populated by ssr-entry.ts) instead of window.location.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\nimport { cachedSearch, cachedSearchParams, _setCachedSearch } from './state.js';\n\nfunction getSearch(): string {\n if (typeof window !== 'undefined') return window.location.search;\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction getServerSearch(): string {\n const data = getSsrData();\n if (!data) return '';\n const sp = new URLSearchParams(data.searchParams);\n const str = sp.toString();\n return str ? `?${str}` : '';\n}\n\nfunction subscribe(callback: () => void): () => void {\n window.addEventListener('popstate', callback);\n return () => window.removeEventListener('popstate', callback);\n}\n\n// Cache the last search string and its parsed URLSearchParams to avoid\n// creating a new object on every render when the URL hasn't changed.\n// State lives in client/state.ts for singleton guarantees.\n\nfunction getSearchParams(): URLSearchParams {\n const search = getSearch();\n if (search !== cachedSearch) {\n const params = new URLSearchParams(search);\n _setCachedSearch(search, params);\n return params;\n }\n return cachedSearchParams;\n}\n\nfunction getServerSearchParams(): URLSearchParams {\n const data = getSsrData();\n return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();\n}\n\n/**\n * Read the current URL search params.\n *\n * Compatible with Next.js's `useSearchParams()` from `next/navigation`.\n */\nexport function useSearchParams(): URLSearchParams {\n // useSyncExternalStore needs a primitive snapshot for comparison.\n // We use the raw search string as the snapshot, then return the\n // parsed URLSearchParams.\n useSyncExternalStore(subscribe, getSearch, getServerSearch);\n return typeof window !== 'undefined' ? getSearchParams() : getServerSearchParams();\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.js';\nimport { usePathname } from './use-pathname.js';\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 { ActionResult, ValidationErrors } from '../server/action-client';\nimport type { FormFlashData } from '../server/form-flash';\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 — matches React 19's useActionState return.\n * [result, formAction, isPending]\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\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 return reactUseActionState(action, initialState as ActionResult<TData> | null, permalink);\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>(\n action: (input: unknown) => Promise<ActionResult<TData>>\n): [(input?: unknown) => Promise<ActionResult<TData>>, boolean] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: unknown): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await action(input);\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── useFormErrors ──────────────────────────────────────────────────────\n\n/** Return type of useFormErrors(). */\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 * Extract per-field and form-level errors from an ActionResult.\n *\n * Pure function (no internal hooks) — follows React naming convention\n * since it's used in render. Accepts the result from `useActionState`\n * or flash data from `getFormFlash()`.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = useFormErrors(result)\n *\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}\n * {errors.formErrors.map(e => <p key={e}>{e}</p>)}\n * </form>\n * )\n * ```\n */\nexport function useFormErrors<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 * useCookie — reactive client-side cookie hook.\n *\n * Uses useSyncExternalStore for SSR-safe, reactive cookie access.\n * All components reading the same cookie name re-render on change.\n * No cross-tab sync (intentional — see design/29-cookies.md).\n *\n * See design/29-cookies.md §\"useCookie(name) Hook\"\n */\n\nimport { useSyncExternalStore } from 'react';\nimport { getSsrData } from './ssr-data.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface ClientCookieOptions {\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Domain scope. Default: omitted (current domain). */\n domain?: string;\n /** Max age in seconds. */\n maxAge?: number;\n /** Expiration date. */\n expires?: Date;\n /** Cross-site policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Only send over HTTPS. Default: true in production. */\n secure?: boolean;\n}\n\nexport type CookieSetter = (value: string, options?: ClientCookieOptions) => void;\n\n// ─── Module-Level Cookie Store ────────────────────────────────────────────\n\ntype Listener = () => void;\n\n/** Per-name subscriber sets. */\nconst listeners = new Map<string, Set<Listener>>();\n\n/** Parse a cookie name from document.cookie. */\nfunction getCookieValue(name: string): string | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n}\n\n/** Serialize options into a cookie string suffix. */\nfunction serializeOptions(options?: ClientCookieOptions): string {\n if (!options) return '; Path=/; SameSite=Lax';\n const parts: string[] = [];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options.secure) parts.push('Secure');\n return '; ' + parts.join('; ');\n}\n\n/** Notify all subscribers for a given cookie name. */\nfunction notify(name: string): void {\n const subs = listeners.get(name);\n if (subs) {\n for (const fn of subs) fn();\n }\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────────────\n\n/**\n * Reactive hook for reading/writing a client-side cookie.\n *\n * Returns `[value, setCookie, deleteCookie]`:\n * - `value`: current cookie value (string | undefined)\n * - `setCookie`: sets the cookie and triggers re-renders\n * - `deleteCookie`: deletes the cookie and triggers re-renders\n *\n * @param name - Cookie name.\n * @param defaultOptions - Default options for setCookie calls.\n */\nexport function useCookie(\n name: string,\n defaultOptions?: ClientCookieOptions\n): [value: string | undefined, setCookie: CookieSetter, deleteCookie: () => void] {\n const subscribe = (callback: Listener): (() => void) => {\n let subs = listeners.get(name);\n if (!subs) {\n subs = new Set();\n listeners.set(name, subs);\n }\n subs.add(callback);\n return () => {\n subs!.delete(callback);\n if (subs!.size === 0) listeners.delete(name);\n };\n };\n\n const getSnapshot = (): string | undefined => getCookieValue(name);\n const getServerSnapshot = (): string | undefined => getSsrData()?.cookies.get(name);\n\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n const setCookie: CookieSetter = (newValue: string, options?: ClientCookieOptions) => {\n const merged = { ...defaultOptions, ...options };\n document.cookie = `${name}=${encodeURIComponent(newValue)}${serializeOptions(merged)}`;\n notify(name);\n };\n\n const deleteCookie = (): void => {\n const path = defaultOptions?.path ?? '/';\n const domain = defaultOptions?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n notify(name);\n };\n\n return [value, setCookie, deleteCookie];\n}\n","// @timber-js/app/client — Client-side primitives\n// These are the primary imports for client components.\n//\n// Framework-internal bootstrap, SSR bridge, and segment plumbing is in\n// #client-internal (Node package import).\n// Design doc: design/triage/api-naming-spike.md §8.5\n\n// JsonSerializable moved to @timber-js/app/codec as the single canonical path.\n// Re-export removed per TIM-721.\nexport type { RenderErrorDigest } from './types';\n\n// Navigation\nexport { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';\nexport { mergePreservedSearchParams } from '../shared/merge-search-params.js';\nexport type { LinkFunction, LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';\nexport type { LinkSegmentParams, OnNavigateHandler, OnNavigateEvent } from './link';\nexport { usePendingNavigation } from './use-pending-navigation';\nexport { useLinkStatus, LinkStatusContext } from './use-link-status';\nexport type { LinkStatus } from './use-link-status';\nexport { useRouter } from './use-router';\nexport type { AppRouterInstance } from './use-router';\nexport { usePathname } from './use-pathname';\nexport { useSearchParams } from './use-search-params';\nexport { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selected-layout-segment';\n\n// Forms\nexport { useActionState, useFormAction, useFormErrors } from './form';\nexport type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';\n\n// Params\nexport { useSegmentParams } from './use-params';\n\n// Query states (URL-synced search params)\nexport { useQueryStates } from './use-query-states';\n\n// Cookies\nexport { useCookie } from './use-cookie';\nexport type { ClientCookieOptions, CookieSetter } from './use-cookie';\n\n// Register the client cookie module with defineCookie's lazy reference.\n// This runs at module load time in the client/SSR environment, wiring up\n// the useCookie hook without a top-level import in define-cookie.ts.\nimport * as _useCookieMod from './use-cookie.js';\nimport { _registerUseCookieModule } from '../cookies/define-cookie.js';\n_registerUseCookieModule(_useCookieMod);\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { t as bindUseQueryStates } from "../_chunks/use-query-states-Lo_s_pw2.js
2
2
  import { n as getSsrData, r as setSsrData, t as clearSsrData } from "../_chunks/ssr-data-DzuI0bIV.js";
3
3
  import { TimberErrorBoundary } from "./error-boundary.js";
4
4
  import { n as useSegmentContext, t as SegmentProvider } from "../_chunks/segment-context-fHFLF1PE.js";
5
- import { a as getNavigationState, f as getRouter, i as NavigationProvider, m as setGlobalRouter, o as setNavigationState, p as getRouterOrNull, r as setHardNavigating, t as setCurrentParams } from "../_chunks/use-params-DrjaGSER.js";
5
+ import { a as getNavigationState, f as getRouter, i as NavigationProvider, m as setGlobalRouter, o as setNavigationState, p as getRouterOrNull, r as setHardNavigating, t as setCurrentParams } from "../_chunks/use-params-Br9YSUFV.js";
6
6
  import { cloneElement, isValidElement } from "react";
7
7
  //#region src/client/segment-cache.ts
8
8
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"link-pending-store.d.ts","sourceRoot":"","sources":["../../src/client/link-pending-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CAC5C;AAMD,+CAA+C;AAC/C,eAAO,MAAM,YAAY,EAAE,UAAgC,CAAC;AAE5D,+BAA+B;AAC/B,eAAO,MAAM,SAAS,EAAE,UAAiC,CAAC;AAc1D;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAIlF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAS1C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAK/E"}
1
+ {"version":3,"file":"link-pending-store.d.ts","sourceRoot":"","sources":["../../src/client/link-pending-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CAC5C;AAMD,+CAA+C;AAC/C,eAAO,MAAM,YAAY,EAAE,UAAgC,CAAC;AAE5D,+BAA+B;AAC/B,eAAO,MAAM,SAAS,EAAE,UAAiC,CAAC;AAc1D;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAGlF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAK/E"}
@@ -1 +1 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/client/link.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-root.d.ts","sourceRoot":"","sources":["../../src/client/navigation-root.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG3F,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAqDlE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEtD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAiCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,GAAG,SAAS,CA0GZ;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI,CAc5F"}
1
+ {"version":3,"file":"navigation-root.d.ts","sourceRoot":"","sources":["../../src/client/navigation-root.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG3F,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAqDlE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEtD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAiCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,GAAG,SAAS,CAyGZ;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI,CAIzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI,CAc5F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.77",
3
+ "version": "0.2.0-alpha.78",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -61,7 +61,6 @@ function getStore(): { current: LinkPendingInstance | null } {
61
61
  export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {
62
62
  const store = getStore();
63
63
  store.current = link;
64
- if (link) console.log('[timber:link-pending] setLink', { store, hasSetter: !!link.setIsPending });
65
64
  }
66
65
 
67
66
  /**
@@ -71,13 +70,7 @@ export function setLinkForCurrentNavigation(link: LinkPendingInstance | null): v
71
70
  */
72
71
  export function activateLinkPending(): void {
73
72
  const store = getStore();
74
- console.log('[timber:link-pending] activate', { hasCurrent: !!store.current, store });
75
- if (store.current) {
76
- console.log('[timber:link-pending] calling setIsPending(LINK_PENDING)');
77
- store.current.setIsPending(LINK_PENDING);
78
- } else {
79
- console.warn('[timber:link-pending] NO store.current — pending state will not fire!');
80
- }
73
+ store.current?.setIsPending(LINK_PENDING);
81
74
  }
82
75
 
83
76
  /**
@@ -550,7 +550,6 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
550
550
  // setIsPending(LINK_PENDING) call happens inside NavigationRoot's
551
551
  // async startTransition via activateLinkPending().
552
552
 
553
- console.log('[timber:link] onClick — setting link for nav', { hasInstance: !!linkInstanceRef.current, hasNavigationApi: hasNavigationApi() });
554
553
  setLinkForCurrentNavigation(linkInstanceRef.current);
555
554
 
556
555
  // When Navigation API is active, let the <a> click propagate
@@ -195,7 +195,6 @@ export function NavigationRoot({
195
195
  // both apply in the same React commit — making the pending→active transition
196
196
  // atomic (no frame where pending is false but the old tree is still visible).
197
197
  _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {
198
- console.log('[timber:nav-root] _navigateTransition called', url);
199
198
  // Urgent: show pending state immediately (for TopLoader / usePendingNavigation)
200
199
  setPendingUrl(url);
201
200
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-params-DrjaGSER.js","names":[],"sources":["../../src/client/router-ref.ts","../../src/client/link-pending-store.ts","../../src/client/navigation-context.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx","../../src/client/use-params.ts"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router.js';\nimport { globalRouter, _setGlobalRouter } from './state.js';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","/**\n * Link Pending Store — per-link optimistic pending state.\n *\n * Tracks which link instance is currently navigating so that only the\n * clicked link shows pending. Uses `useOptimistic` from React 19 —\n * the optimistic value (isPending=true) is set inside NavigationRoot's\n * async startTransition so it persists for the duration of the RSC\n * fetch, then auto-reverts to idle when the transition settles.\n *\n * Flow:\n * 1. Link click → setLinkForCurrentNavigation(instance) stores the ref\n * 2. NavigationRoot startTransition → activateLinkPending() calls\n * instance.setIsPending(LINK_PENDING) inside the async transition\n * 3. Transition settles → useOptimistic auto-reverts to LINK_IDLE\n *\n * SINGLETON GUARANTEE: Uses `globalThis` via `Symbol.for` keys because\n * the RSC client bundler can duplicate this module across chunks.\n *\n * See design/19-client-navigation.md §\"Per-Link Pending State\"\n */\n\nimport type { LinkStatus } from './use-link-status.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface LinkPendingInstance {\n setIsPending: (status: LinkStatus) => void;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nconst LINK_PENDING_KEY = Symbol.for('__timber_link_pending');\n\n/** Status object: link navigation in flight */\nexport const LINK_PENDING: LinkStatus = { isPending: true };\n\n/** Status object: link idle */\nexport const LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Singleton Storage ───────────────────────────────────────────\n\nfunction getStore(): { current: LinkPendingInstance | null } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[LINK_PENDING_KEY]) {\n g[LINK_PENDING_KEY] = { current: null };\n }\n return g[LINK_PENDING_KEY] as { current: LinkPendingInstance | null };\n}\n\n// ─── Public API ──────────────────────────────────────────────────\n\n/**\n * Register the link instance that initiated the current navigation.\n * Called from Link's click handler. Does NOT call setIsPending —\n * that happens inside NavigationRoot's startTransition via\n * activateLinkPending().\n *\n * Resets the previous link to idle immediately (the old link's\n * useOptimistic reverts since its transition already settled).\n */\nexport function setLinkForCurrentNavigation(link: LinkPendingInstance | null): void {\n const store = getStore();\n store.current = link;\n if (link) console.log('[timber:link-pending] setLink', { store, hasSetter: !!link.setIsPending });\n}\n\n/**\n * Activate pending state on the current link instance.\n * MUST be called inside NavigationRoot's async startTransition —\n * this is what makes useOptimistic persist for the navigation duration.\n */\nexport function activateLinkPending(): void {\n const store = getStore();\n console.log('[timber:link-pending] activate', { hasCurrent: !!store.current, store });\n if (store.current) {\n console.log('[timber:link-pending] calling setIsPending(LINK_PENDING)');\n store.current.setIsPending(LINK_PENDING);\n } else {\n console.warn('[timber:link-pending] NO store.current — pending state will not fire!');\n }\n}\n\n/**\n * Reset the current link's pending state. With useOptimistic this is\n * handled automatically when the transition settles. Kept for callers\n * that explicitly need to clear on error paths.\n */\nexport function resetLinkPending(): void {\n const store = getStore();\n if (store.current) {\n store.current.setIsPending(LINK_IDLE);\n store.current = null;\n }\n}\n\n/**\n * @deprecated No longer needed with useOptimistic. Kept for callers.\n */\nexport function getCurrentNavId(): number {\n return 0;\n}\n\n/**\n * Clean up the link pending store entirely.\n */\nexport function clearLinkPendingSetter(): void {\n getStore().current = null;\n}\n\n/**\n * Unmount a link instance from navigation tracking.\n */\nexport function unmountLinkForCurrentNavigation(link: LinkPendingInstance): void {\n const store = getStore();\n if (store.current === link) {\n store.current = null;\n }\n}\n","'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current route params and pathname, updated atomically\n * with the RSC tree on each navigation. This replaces the previous\n * useSyncExternalStore approach for useSegmentParams() and usePathname(),\n * which suffered from a timing gap: the new tree could commit before\n * the external store re-renders fired, causing a frame where both\n * old and new active states were visible simultaneously.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (useParams, usePathname) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, no NavigationProvider is mounted. Hooks fall back to\n * the ALS-backed getSsrData() for per-request isolation.\n *\n * IMPORTANT: createContext and useContext are NOT available in the RSC\n * environment (React Server Components use a stripped-down React).\n * The context is lazily initialized on first access, and all functions\n * that depend on these APIs are safe to call from any environment —\n * they return null or no-op when the APIs aren't available.\n *\n * SINGLETON GUARANTEE: All shared mutable state uses globalThis via\n * Symbol.for keys. The RSC client bundler can duplicate this module\n * across chunks (browser-entry graph + client-reference graph). With\n * ESM output, each chunk gets its own module scope — module-level\n * variables would create separate singleton instances per chunk.\n * globalThis guarantees a single instance regardless of duplication.\n *\n * This workaround will be removed when Rolldown ships `format: 'app'`\n * (module registry format that deduplicates like webpack/Turbopack).\n * See design/27-chunking-strategy.md.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport React, { createElement, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\nexport interface NavigationState {\n params: Record<string, string | string[]>;\n pathname: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * The context is created lazily to avoid calling createContext at module\n * level. In the RSC environment, React.createContext doesn't exist —\n * calling it at import time would crash the server.\n *\n * Context instances are stored on globalThis (NOT in module-level\n * variables) because the ESM bundler can duplicate this module across\n * chunks. Module-level variables would create separate instances per\n * chunk — the provider in NavigationRoot (index chunk) would use\n * context A while the consumer in usePendingNavigation (shared chunk)\n * reads from context B. globalThis guarantees a single instance.\n *\n * See design/27-chunking-strategy.md §\"Singleton Safety\"\n *\n * NOTE: Despite similar naming, `usePendingNavigationUrl()` here is an\n * internal helper — the public hook is `usePendingNavigation()` in\n * use-pending-navigation.ts.\n */\n\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\nconst PENDING_CTX_KEY = Symbol.for('__timber_pending_nav_ctx');\n\nfunction getOrCreateContext(): React.Context<NavigationState | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | React.Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n // createContext may not exist in the RSC environment\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the navigation context. Returns null during SSR (no provider)\n * or in the RSC environment (no context available).\n * Internal — used by useSegmentParams() and usePathname().\n */\nexport function useNavigationContext(): NavigationState | null {\n const ctx = getOrCreateContext();\n if (!ctx) return null;\n // useContext may not exist in the RSC environment — caller wraps in try/catch\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({\n value,\n children,\n}: NavigationProviderProps): React.ReactElement {\n const ctx = getOrCreateContext();\n if (!ctx) {\n // RSC environment — no context available. Return children as-is.\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation state communicated between the router and renderRoot.\n *\n * The router calls setNavigationState() before renderRoot(). The\n * renderRoot callback reads via getNavigationState() to create the\n * NavigationProvider with the correct params/pathname.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n *\n * Stored on globalThis (like the context instances above) because the\n * router lives in one chunk while renderRoot lives in another. Module-\n * level variables would be separate per chunk.\n */\nconst NAV_STATE_KEY = Symbol.for('__timber_nav_state');\n\nfunction _getNavStateStore(): { current: NavigationState } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_STATE_KEY]) {\n g[NAV_STATE_KEY] = { current: { params: {}, pathname: '/' } };\n }\n return g[NAV_STATE_KEY] as { current: NavigationState };\n}\n\nexport function setNavigationState(state: NavigationState): void {\n _getNavStateStore().current = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _getNavStateStore().current;\n}\n\n// ---------------------------------------------------------------------------\n// Pending Navigation Context (same module for singleton guarantee)\n// ---------------------------------------------------------------------------\n\n/**\n * Separate context for the in-flight navigation URL. Provided by\n * NavigationRoot (urgent useState), consumed by usePendingNavigation\n * and TopLoader. Per-link pending state uses useOptimistic instead\n * (see link-pending-store.ts).\n *\n * Uses globalThis via Symbol.for for the same reason as NavigationContext\n * above — the bundler may duplicate this module across chunks, and module-\n * level variables would create separate context instances.\n */\n\nfunction getOrCreatePendingContext(): React.Context<string | null> | undefined {\n const existing = (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] as\n | React.Context<string | null>\n | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext === 'function') {\n const ctx = React.createContext<string | null>(null);\n (globalThis as Record<symbol, unknown>)[PENDING_CTX_KEY] = ctx;\n return ctx;\n }\n return undefined;\n}\n\n/**\n * Read the pending navigation URL from context.\n * Returns null during SSR (no provider) or in the RSC environment.\n */\nexport function usePendingNavigationUrl(): string | null {\n const ctx = getOrCreatePendingContext();\n if (!ctx) return null;\n if (typeof React.useContext !== 'function') return null;\n return React.useContext(ctx);\n}\n\n/**\n * Provider for the pending navigation URL. Wraps children with\n * the pending context Provider.\n */\nexport function PendingNavigationProvider({\n value,\n children,\n}: {\n value: string | null;\n children?: ReactNode;\n}): React.ReactElement {\n const ctx = getOrCreatePendingContext();\n if (!ctx) {\n return children as React.ReactElement;\n }\n return createElement(ctx.Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Navigation API transition state (optional progressive enhancement)\n// ---------------------------------------------------------------------------\n\n/**\n * Check if the browser's Navigation API has an active transition.\n *\n * When the Navigation API is available and a navigation has been intercepted\n * via event.intercept(), `navigation.transition` is non-null until the\n * handler resolves. This provides browser-native progress tracking that\n * can be used alongside the existing pendingUrl mechanism.\n *\n * Returns false when Navigation API is unavailable or no transition is active.\n */\nexport function hasNativeNavigationTransition(): boolean {\n if (typeof window === 'undefined') return false;\n const nav = (window as unknown as { navigation?: { transition?: unknown } }).navigation;\n return nav?.transition != null;\n}\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigationUrl, hasNativeNavigationTransition } from './navigation-context.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from PendingNavigationContext.\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n const pendingUrl = usePendingNavigationUrl();\n // Navigation is pending when either:\n // 1. Our React-based pending URL is set (standard path), OR\n // 2. The Navigation API has an active transition (external navigations\n // intercepted by the navigate event that haven't completed yet).\n // In practice these are almost always in sync — the Navigation API\n // transition is active while our pendingUrl is set. This check ensures\n // the top-loader also shows for external navigations caught by the\n // Navigation API before our React state updates.\n const isPending = pendingUrl !== null || hasNativeNavigationTransition();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — Wrapper component for transition-based rendering.\n *\n * Solves the \"new boundary has no old content\" problem for client-side\n * navigation. When React renders a completely new Suspense boundary via\n * root.render(), it shows the fallback immediately — root.render() is\n * always an urgent update regardless of startTransition.\n *\n * NavigationRoot holds the current element in React state. Navigation\n * updates call startTransition(() => setState(newElement)), which IS\n * a transition update. React keeps the old committed tree visible while\n * any new Suspense boundaries in the transition resolve.\n *\n * Also manages `pendingUrl` as React state with an urgent/transition split:\n * - Navigation START: `setPendingUrl(url)` is an urgent update — React\n * commits it before the next paint, showing the spinner immediately.\n * - Navigation END: `setPendingUrl(null)` is inside `startTransition`\n * alongside `setElement(newTree)` — both commit atomically, so the\n * spinner disappears in the same frame as the new content appears.\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, startTransition, useState, type ReactNode } from 'react';\nimport { activateLinkPending, resetLinkPending } from './link-pending-store.js';\nimport { PendingNavigationProvider } from './navigation-context.js';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.js';\n\n// ─── Navigation Transition Counter ──────────────────────────────\n// Monotonically increasing counter that increments each time\n// navigateTransition() is called. Used to detect stale transitions:\n// if a newer transition started while the current one's perform()\n// was in flight, the current transition is stale and should reject.\n//\n// Separate from the link-pending navId (which only increments on\n// link clicks). This counter covers all navigation types: link clicks,\n// programmatic navigate(), refresh(), and handlePopState().\n//\n// Uses globalThis for singleton guarantee across chunks — same pattern\n// as NavigationContext and the link pending store.\n\nconst NAV_TRANSITION_KEY = Symbol.for('__timber_nav_transition_counter');\n\nfunction getTransitionCounter(): { id: number } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_TRANSITION_KEY]) {\n g[NAV_TRANSITION_KEY] = { id: 0 };\n }\n return g[NAV_TRANSITION_KEY] as { id: number };\n}\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Module-level functions ──────────────────────────────────────\n\n/**\n * Module-level reference to the state setter wrapped in startTransition.\n * Used for non-navigation renders (applyRevalidation, popstate replay).\n */\nlet _transitionRender: ((element: ReactNode) => void) | null = null;\n\n/**\n * Module-level reference to the navigation transition function.\n * Wraps a full navigation (fetch + render) in a single startTransition\n * with the pending URL.\n */\nlet _navigateTransition:\n | ((pendingUrl: string, perform: () => Promise<ReactNode>) => Promise<void>)\n | null = null;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root wrapper component that enables transition-based rendering.\n *\n * Renders PendingNavigationProvider around children for the pending URL\n * context. The DOM tree matches the server-rendered HTML during hydration\n * (the provider renders no extra DOM elements).\n *\n * Usage in browser-entry.ts:\n * const rootEl = createElement(NavigationRoot, { initial: wrapped });\n * reactRoot = hydrateRoot(document, rootEl);\n *\n * Subsequent navigations:\n * navigateTransition(url, async () => { fetch; return wrappedElement; });\n *\n * Non-navigation renders:\n * transitionRender(newWrappedElement);\n */\nexport function NavigationRoot({\n initial,\n topLoaderConfig,\n}: {\n initial: ReactNode;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n const [element, setElement] = useState<ReactNode>(initial);\n const [pendingUrl, setPendingUrl] = useState<string | null>(null);\n\n // NOTE: We use standalone `startTransition` (imported from 'react'),\n // NOT `useTransition`. The `useTransition` hook's `startTransition`\n // is tied to a single fiber and tracks one async callback at a time.\n // When two navigations overlap (click slow-page, then click dashboard),\n // calling useTransition's startTransition twice with concurrent async\n // callbacks corrupts React's internal hook tracking — causing\n // \"Rendered more hooks than during the previous render.\"\n //\n // Standalone `startTransition` creates independent transition lanes\n // for each call, so concurrent navigations don't interfere. We don't\n // need useTransition's `isPending` — we track pending state via our\n // own `pendingUrl` useState.\n //\n // This matches the Next.js pattern (TIM-625): \"No useTransition in\n // the router at all — only standalone startTransition.\"\n\n // Non-navigation render (revalidation, popstate cached replay).\n _transitionRender = (newElement: ReactNode) => {\n startTransition(() => {\n setElement(newElement);\n });\n };\n\n // Full navigation transition.\n // setPendingUrl(url) is an URGENT update — React commits it before the next\n // paint, so the pending spinner appears immediately when navigation starts.\n // Inside startTransition: the async fetch + setElement + setPendingUrl(null)\n // are deferred. When the transition commits, the new tree and pendingUrl=null\n // both apply in the same React commit — making the pending→active transition\n // atomic (no frame where pending is false but the old tree is still visible).\n _navigateTransition = (url: string, perform: () => Promise<ReactNode>) => {\n console.log('[timber:nav-root] _navigateTransition called', url);\n // Urgent: show pending state immediately (for TopLoader / usePendingNavigation)\n setPendingUrl(url);\n\n // Increment the transition counter SYNCHRONOUSLY (before startTransition\n // schedules the async work). Each call gets a unique transId; the counter\n // is the same globalThis singleton, so a newer call always has a higher id.\n const counter = getTransitionCounter();\n const transId = ++counter.id;\n\n return new Promise<void>((resolve, reject) => {\n startTransition(async () => {\n // Activate per-link pending state inside this async transition.\n // useOptimistic persists the isPending=true value for the duration\n // of this transition, then auto-reverts when it settles.\n activateLinkPending();\n try {\n const newElement = await perform();\n // Only commit state if this is still the active navigation.\n // A superseded transition's updates must be dropped entirely.\n if (counter.id === transId) {\n setElement(newElement);\n setPendingUrl(null);\n resolve();\n } else {\n // Stale transition — a newer navigation has superseded this one.\n // Reject so the caller (navigate/refresh/handlePopState) doesn't\n // run post-transition side effects (applyHead, scroll, event\n // dispatch) with stale data. All callers catch AbortError.\n reject(new DOMException('Navigation superseded', 'AbortError'));\n }\n } catch (err) {\n // Only clear pending if this is still the active navigation.\n if (counter.id === transId) {\n setPendingUrl(null);\n resetLinkPending();\n }\n reject(err);\n }\n });\n });\n };\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the two\n // useState calls above). React requires the same hooks to run on\n // every render. If we threw before hooks, React would see 0 hooks\n // on the re-render vs 2 hooks on the initial render — triggering\n // the exact \"Rendered more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree inside PendingNavigationProvider.\n // The TopLoader reads pendingUrl from context to show/hide the progress bar.\n // It is rendered only when not explicitly disabled via config.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n const children = showTopLoader\n ? createElement(Fragment, null, createElement(TopLoader, { config: topLoaderConfig }), element)\n : element;\n return createElement(PendingNavigationProvider, { value: pendingUrl }, children);\n}\n\n// ─── Public API ──────────────────────────────────────────────────\n\n/**\n * Trigger a transition render for non-navigation updates.\n * React keeps the old committed tree visible while any new Suspense\n * boundaries in the update resolve.\n *\n * Used for: applyRevalidation, popstate replay with cached payload.\n */\nexport function transitionRender(element: ReactNode): void {\n if (_transitionRender) {\n _transitionRender(element);\n }\n}\n\n/**\n * Run a full navigation inside a React transition with optimistic pending URL.\n *\n * The `perform` callback runs inside `startTransition` — it should fetch the\n * RSC payload, update router state, and return the wrapped React element.\n * The pending URL shows immediately (urgent update) and reverts\n * to null when the transition commits (atomic with the new tree).\n *\n * Returns a Promise that resolves when the async work completes (note: the\n * React transition may not have committed yet, but all state updates are done).\n *\n * Used for: navigate(), refresh(), popstate with fetch.\n */\nexport function navigateTransition(\n pendingUrl: string,\n perform: () => Promise<ReactNode>\n): Promise<void> {\n if (_navigateTransition) {\n return _navigateTransition(pendingUrl, perform);\n }\n // Fallback: no NavigationRoot mounted (shouldn't happen in production)\n return perform().then(() => {});\n}\n\n/**\n * Check if the NavigationRoot is mounted and ready for renders.\n * Used by browser-entry.ts to guard against renders before hydration.\n */\nexport function isNavigationRootReady(): boolean {\n return _transitionRender !== null;\n}\n\n/**\n * Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).\n *\n * When there's no RSC payload, we can't create a React root immediately —\n * `createRoot(document).render(...)` would blank the SSR HTML. Instead,\n * this sets up `_transitionRender` and `_navigateTransition` so that the\n * first client navigation triggers root creation via `createAndMount`.\n *\n * After `createAndMount` runs, NavigationRoot renders and overwrites these\n * callbacks with its real `startTransition`-based implementations.\n */\nexport function installDeferredNavigation(createAndMount: (initial: ReactNode) => void): void {\n let mounted = false;\n const mountOnce = (element: ReactNode) => {\n if (mounted) return;\n mounted = true;\n createAndMount(element);\n };\n _transitionRender = (element: ReactNode) => {\n mountOnce(element);\n };\n _navigateTransition = async (_pendingUrl: string, perform: () => Promise<ReactNode>) => {\n const element = await perform();\n mountOnce(element);\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 * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from NavigationContext\n * which is updated atomically with the RSC tree render. This replaces\n * the previous useSyncExternalStore approach that suffered from a\n * timing gap between tree render and store notification — causing\n * preserved layout components to briefly show stale active state.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport { currentParams, _setCurrentParams, paramsListeners } from './state.js';\nimport { useNavigationContext } from './navigation-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\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 * On the client, reads from NavigationContext (provided by\n * NavigationProvider in renderRoot). This ensures params update\n * atomically with the RSC tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known route 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 route: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(route?: string): Record<string, string | string[]>;\nexport function useSegmentParams(_route?: string): Record<string, string | string[]> {\n // Try reading from NavigationContext (client-side, inside React tree).\n // During SSR, no NavigationProvider is mounted, so this returns null.\n // When called outside a React component, useContext throws — caught below.\n try {\n const navContext = useNavigationContext();\n if (navContext !== null) {\n return navContext.params;\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n return getSsrData()?.params ?? currentParams;\n}\n"],"mappings":"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"}