@timber-js/app 0.1.23 → 0.1.24

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.
@@ -19,8 +19,6 @@ var currentParams = {};
19
19
  function _setCurrentParams(params) {
20
20
  currentParams = params;
21
21
  }
22
- /** Listeners notified when currentParams changes. */
23
- var paramsListeners = /* @__PURE__ */ new Set();
24
22
  /** Cached search string — avoids reparsing when URL hasn't changed. */
25
23
  var cachedSearch = "";
26
24
  var cachedSearchParams = new URLSearchParams();
@@ -85,6 +83,6 @@ function getSsrData() {
85
83
  return currentSsrData;
86
84
  }
87
85
  //#endregion
88
- export { _setCurrentParams as a, cachedSearchParams as c, paramsListeners as d, _setCachedSearch as i, currentParams as l, getSsrData as n, _setGlobalRouter as o, setSsrData as r, cachedSearch as s, clearSsrData as t, globalRouter as u };
86
+ export { _setCurrentParams as a, cachedSearchParams as c, _setCachedSearch as i, currentParams as l, getSsrData as n, _setGlobalRouter as o, setSsrData as r, cachedSearch as s, clearSsrData as t, globalRouter as u };
89
87
 
90
- //# sourceMappingURL=ssr-data-B2yikEEB.js.map
88
+ //# sourceMappingURL=ssr-data-DLnbYpj1.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-data-B2yikEEB.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { RouterInstance } from './router.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: Record<string, string | string[]> = {};\n\nexport function _setCurrentParams(params: Record<string, string | string[]>): void {\n currentParams = params;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\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 {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Mutable reference to NavContext for error boundary → RSC communication.\n * When TimberErrorBoundary catches a DenySignal, it sets\n * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry\n * from promoting the denial to page-level. See LOCAL-298.\n */\n _navContext?: { _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA2BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;AACpE,gBAAe;;;;;;AASjB,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;AAClE,kBAAiB;;;AAMnB,IAAW,gBAAmD,EAAE;AAEhE,SAAgB,kBAAkB,QAAiD;AACjF,iBAAgB;;;AAIlB,IAAa,kCAAkB,IAAI,KAAiB;;AAKpD,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,iBAAiB;AAErD,SAAgB,iBAAiB,QAAgB,QAA+B;AAC9E,gBAAe;AACf,sBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACiBvB,SAAgB,WAAW,MAAqB;AAC9C,oBAAmB,KAAK;;;;;;;;AAS1B,SAAgB,eAAqB;AACnC,oBAAmB,KAAA,EAAU;;;;;;;;;;;AAY/B,SAAgB,aAAkC;AAChD,KAAI,gBACF,QAAO,iBAAiB;AAE1B,QAAO"}
1
+ {"version":3,"file":"ssr-data-DLnbYpj1.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { RouterInstance } from './router.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: Record<string, string | string[]> = {};\n\nexport function _setCurrentParams(params: Record<string, string | string[]>): void {\n currentParams = params;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\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 {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Mutable reference to NavContext for error boundary → RSC communication.\n * When TimberErrorBoundary catches a DenySignal, it sets\n * `_navContext._denyHandledByBoundary = true` to prevent the RSC entry\n * from promoting the denial to page-level. See LOCAL-298.\n */\n _navContext?: { _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA2BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;AACpE,gBAAe;;;;;;AASjB,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;AAClE,kBAAiB;;;AAMnB,IAAW,gBAAmD,EAAE;AAEhE,SAAgB,kBAAkB,QAAiD;AACjF,iBAAgB;;;AASlB,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,iBAAiB;AAErD,SAAgB,iBAAiB,QAAgB,QAA+B;AAC9E,gBAAe;AACf,sBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACiBvB,SAAgB,WAAW,MAAqB;AAC9C,oBAAmB,KAAK;;;;;;;;AAS1B,SAAgB,eAAqB;AACnC,oBAAmB,KAAA,EAAU;;;;;;;;;;;AAY/B,SAAgB,aAAkC;AAChD,KAAI,gBACF,QAAO,iBAAiB;AAE1B,QAAO"}
@@ -1,4 +1,4 @@
1
- import { n as getSsrData } from "./ssr-data-B2yikEEB.js";
1
+ import { n as getSsrData } from "./ssr-data-DLnbYpj1.js";
2
2
  import { useSyncExternalStore } from "react";
3
3
  //#region src/client/use-cookie.ts
4
4
  /**
@@ -88,4 +88,4 @@ function useCookie(name, defaultOptions) {
88
88
  //#endregion
89
89
  export { useCookie as t };
90
90
 
91
- //# sourceMappingURL=use-cookie-D5aS4slY.js.map
91
+ //# sourceMappingURL=use-cookie-dDbpCTx-.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-cookie-D5aS4slY.js","names":[],"sources":["../../src/client/use-cookie.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;;AAqCA,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}
1
+ {"version":3,"file":"use-cookie-dDbpCTx-.js","names":[],"sources":["../../src/client/use-cookie.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;;AAqCA,IAAM,4BAAY,IAAI,KAA4B;;AAGlD,SAAS,eAAe,MAAkC;AACxD,KAAI,OAAO,aAAa,YAAa,QAAO,KAAA;CAC5C,MAAM,QAAQ,SAAS,OAAO,MAC5B,IAAI,OAAO,gBAAgB,KAAK,QAAQ,uBAAuB,OAAO,GAAG,mBAAmB,CAC7F;AACD,QAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG,KAAA;;;AAIhD,SAAS,iBAAiB,SAAuC;AAC/D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,QAAkB,EAAE;AAC1B,OAAM,KAAK,QAAQ,QAAQ,QAAQ,MAAM;AACzC,KAAI,QAAQ,OAAQ,OAAM,KAAK,UAAU,QAAQ,SAAS;AAC1D,KAAI,QAAQ,WAAW,KAAA,EAAW,OAAM,KAAK,WAAW,QAAQ,SAAS;AACzE,KAAI,QAAQ,QAAS,OAAM,KAAK,WAAW,QAAQ,QAAQ,aAAa,GAAG;CAC3E,MAAM,WAAW,QAAQ,YAAY;AACrC,OAAM,KAAK,YAAY,SAAS,OAAO,EAAE,CAAC,aAAa,GAAG,SAAS,MAAM,EAAE,GAAG;AAC9E,KAAI,QAAQ,OAAQ,OAAM,KAAK,SAAS;AACxC,QAAO,OAAO,MAAM,KAAK,KAAK;;;AAIhC,SAAS,OAAO,MAAoB;CAClC,MAAM,OAAO,UAAU,IAAI,KAAK;AAChC,KAAI,KACF,MAAK,MAAM,MAAM,KAAM,KAAI;;;;;;;;;;;;;AAiB/B,SAAgB,UACd,MACA,gBACgF;CAChF,MAAM,aAAa,aAAqC;EACtD,IAAI,OAAO,UAAU,IAAI,KAAK;AAC9B,MAAI,CAAC,MAAM;AACT,0BAAO,IAAI,KAAK;AAChB,aAAU,IAAI,MAAM,KAAK;;AAE3B,OAAK,IAAI,SAAS;AAClB,eAAa;AACX,QAAM,OAAO,SAAS;AACtB,OAAI,KAAM,SAAS,EAAG,WAAU,OAAO,KAAK;;;CAIhD,MAAM,oBAAwC,eAAe,KAAK;CAClE,MAAM,0BAA8C,YAAY,EAAE,QAAQ,IAAI,KAAK;CAEnF,MAAM,QAAQ,qBAAqB,WAAW,aAAa,kBAAkB;CAE7E,MAAM,aAA2B,UAAkB,YAAkC;EACnF,MAAM,SAAS;GAAE,GAAG;GAAgB,GAAG;GAAS;AAChD,WAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,SAAS,GAAG,iBAAiB,OAAO;AACpF,SAAO,KAAK;;CAGd,MAAM,qBAA2B;EAC/B,MAAM,OAAO,gBAAgB,QAAQ;EACrC,MAAM,SAAS,gBAAgB;EAC/B,IAAI,YAAY,GAAG,KAAK,4DAA4D;AACpF,MAAI,OAAQ,cAAa,YAAY;AACrC,WAAS,SAAS;AAClB,SAAO,KAAK;;AAGd,QAAO;EAAC;EAAO;EAAW;EAAa"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  "use client";
3
- import { n as getSsrData } from "../_chunks/ssr-data-B2yikEEB.js";
3
+ import { n as getSsrData } from "../_chunks/ssr-data-DLnbYpj1.js";
4
4
  import { Component, createElement } from "react";
5
5
  //#region src/client/error-boundary.tsx
6
6
  /**
@@ -22,6 +22,8 @@ export type { HistoryEntry } from './history';
22
22
  export { useActionState, useFormAction, useFormErrors } from './form';
23
23
  export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
24
24
  export { useParams, setCurrentParams } from './use-params';
25
+ export { NavigationProvider, NavigationContext, getNavigationState, setNavigationState } from './navigation-context';
26
+ export type { NavigationState } from './navigation-context';
25
27
  export { useQueryStates, bindUseQueryStates } from './use-query-states';
26
28
  export { useCookie } from './use-cookie';
27
29
  export type { ClientCookieOptions, CookieSetter } from './use-cookie';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrH,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,11 +1,10 @@
1
1
  "use client";
2
- import { a as _setCurrentParams, c as cachedSearchParams, d as paramsListeners, i as _setCachedSearch, l as currentParams, n as getSsrData, o as _setGlobalRouter, r as setSsrData, s as cachedSearch, t as clearSsrData, u as globalRouter } from "../_chunks/ssr-data-B2yikEEB.js";
2
+ import { a as _setCurrentParams, c as cachedSearchParams, i as _setCachedSearch, l as currentParams, n as getSsrData, o as _setGlobalRouter, r as setSsrData, s as cachedSearch, t as clearSsrData, u as globalRouter } from "../_chunks/ssr-data-DLnbYpj1.js";
3
3
  import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-DAhgj8Gx.js";
4
- import { t as useCookie } from "../_chunks/use-cookie-D5aS4slY.js";
4
+ import { t as useCookie } from "../_chunks/use-cookie-dDbpCTx-.js";
5
5
  import { TimberErrorBoundary } from "./error-boundary.js";
6
6
  import { createContext, createElement, startTransition, useActionState as useActionState$1, useContext, useEffect, useMemo, useRef, useSyncExternalStore, useTransition } from "react";
7
7
  import { jsxDEV } from "react/jsx-dev-runtime";
8
- import { flushSync } from "react-dom";
9
8
  //#region src/client/link-navigate-interceptor.tsx
10
9
  var _jsxFileName$2 = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link-navigate-interceptor.tsx";
11
10
  /** Symbol used to store the onNavigate callback on anchor elements. */
@@ -380,94 +379,94 @@ var HistoryStack = class {
380
379
  }
381
380
  };
382
381
  //#endregion
383
- //#region src/client/use-params.ts
382
+ //#region src/client/navigation-context.ts
384
383
  /**
385
- * useParams()client-side hook for accessing route params.
386
- *
387
- * Returns the dynamic route parameters for the current URL.
388
- * When called with a route pattern argument, TypeScript narrows
389
- * the return type to the exact params shape for that route.
390
- *
391
- * Two layers of type narrowing work together:
392
- * 1. The generic overload here uses the Routes interface directly —
393
- * `useParams<R>()` returns `Routes[R]['params']`.
394
- * 2. Build-time codegen generates per-route string-literal overloads
395
- * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).
384
+ * NavigationContextReact context for navigation state.
396
385
  *
397
- * When the Routes interface is empty (no codegen yet), the generic
398
- * overload has `keyof Routes = never`, so only the fallback matches.
386
+ * Holds the current route params and pathname, updated atomically
387
+ * with the RSC tree on each navigation. This replaces the previous
388
+ * useSyncExternalStore approach for useParams() and usePathname(),
389
+ * which suffered from a timing gap: the new tree could commit before
390
+ * the external store re-renders fired, causing a frame where both
391
+ * old and new active states were visible simultaneously.
399
392
  *
400
- * During SSR, params are read from the ALS-backed SSR data context
401
- * (populated by ssr-entry.ts) to ensure correct per-request isolation
402
- * across concurrent requests with streaming Suspense.
393
+ * By wrapping the RSC payload element in NavigationProvider inside
394
+ * renderRoot(), the context value and the element tree are passed to
395
+ * reactRoot.render() in the same call — atomic by construction.
396
+ * All consumers (useParams, usePathname) see the new values in the
397
+ * same render pass as the new tree.
403
398
  *
404
- * Reactivity: useParams() uses useSyncExternalStore so that components
405
- * in unchanged layouts (e.g., sidebar items) re-render atomically when
406
- * params change during client-side navigation. This matches the pattern
407
- * used by usePathname() and useSearchParams().
408
- *
409
- * All mutable state is delegated to client/state.ts for singleton guarantees.
410
- * See design/18-build-system.md §"Singleton State Registry"
399
+ * During SSR, no NavigationProvider is mounted. Hooks fall back to
400
+ * the ALS-backed getSsrData() for per-request isolation.
411
401
  *
412
- * Design doc: design/09-typescript.md §"Typed Routes"
402
+ * See design/19-client-navigation.md §"Navigation Flow"
413
403
  */
414
404
  /**
415
- * Subscribe to params changes. Called by useSyncExternalStore.
416
- * Exported for testing not intended for direct use by app code.
405
+ * The context value is null when no provider is mounted (SSR).
406
+ * On the client, NavigationProvider always wraps the tree.
417
407
  */
418
- function subscribe$2(callback) {
419
- paramsListeners.add(callback);
420
- return () => paramsListeners.delete(callback);
408
+ var NavigationContext = createContext(null);
409
+ /**
410
+ * Read the navigation context. Returns null during SSR (no provider).
411
+ * Internal — used by useParams() and usePathname().
412
+ */
413
+ function useNavigationContext() {
414
+ return useContext(NavigationContext);
421
415
  }
422
416
  /**
423
- * Get the current params snapshot (client).
424
- * Exported for testing — not intended for direct use by app code.
417
+ * Wraps children with NavigationContext.Provider.
418
+ *
419
+ * Used in browser-entry.ts renderRoot to wrap the RSC payload element
420
+ * so that navigation state updates atomically with the tree render.
425
421
  */
426
- function getSnapshot() {
427
- return currentParams;
422
+ function NavigationProvider({ value, children }) {
423
+ return createElement(NavigationContext.Provider, { value }, children);
428
424
  }
429
425
  /**
430
- * Get the server-side params snapshot (SSR).
431
- * Falls back to the module-level currentParams if no SSR context
432
- * is available (shouldn't happen, but defensive).
426
+ * Module-level navigation state. Updated by the router before calling
427
+ * renderRoot(). The renderRoot callback reads this to create the
428
+ * NavigationProvider with the correct values.
429
+ *
430
+ * This is NOT used by hooks directly — hooks read from React context.
431
+ * This exists only as a communication channel between the router
432
+ * (which knows the new nav state) and renderRoot (which wraps the element).
433
433
  */
434
- function getServerSnapshot() {
435
- return getSsrData()?.params ?? currentParams;
434
+ var _currentNavState = {
435
+ params: {},
436
+ pathname: "/"
437
+ };
438
+ function setNavigationState(state) {
439
+ _currentNavState = state;
436
440
  }
441
+ function getNavigationState() {
442
+ return _currentNavState;
443
+ }
444
+ //#endregion
445
+ //#region src/client/use-params.ts
437
446
  /**
438
- * Set the current route params WITHOUT notifying subscribers.
439
- * Called by the router before renderPayload() so that new components
440
- * in the RSC tree see the updated params via getSnapshot(), but
441
- * preserved layout components don't re-render prematurely with
442
- * {old tree, new params}.
447
+ * Set the current route params in the module-level store.
443
448
  *
444
- * After the React render commits, the router calls notifyParamsListeners()
445
- * to trigger re-renders in preserved layouts that read useParams().
449
+ * Called by the router on each navigation. This updates the fallback
450
+ * snapshot used by tests and by the hook when called outside a React
451
+ * component (no NavigationContext available).
452
+ *
453
+ * On the client, the primary reactivity path is NavigationContext —
454
+ * the router calls setNavigationState() then renderRoot() which wraps
455
+ * the element in NavigationProvider. setCurrentParams is still called
456
+ * for the module-level fallback.
446
457
  *
447
- * On the client, the segment router calls this on each navigation.
448
458
  * During SSR, params are also available via getSsrData().params
449
- * (ALS-backed), but setCurrentParams is still called for the
450
- * module-level fallback path.
459
+ * (ALS-backed).
451
460
  */
452
461
  function setCurrentParams(params) {
453
462
  _setCurrentParams(params);
454
463
  }
455
- /**
456
- * Notify all useSyncExternalStore subscribers that params have changed.
457
- * Called by the router AFTER renderPayload() so that preserved layout
458
- * components re-render only after the new tree is committed — producing
459
- * an atomic {new tree, new params} update instead of a stale
460
- * {old tree, new params} intermediate state.
461
- */
462
- function notifyParamsListeners() {
463
- for (const listener of paramsListeners) listener();
464
- }
465
464
  function useParams(_route) {
466
465
  try {
467
- return useSyncExternalStore(subscribe$2, getSnapshot, getServerSnapshot);
468
- } catch {
469
- return getServerSnapshot();
470
- }
466
+ const navContext = useNavigationContext();
467
+ if (navContext !== null) return navContext.params;
468
+ } catch {}
469
+ return getSsrData()?.params ?? currentParams;
471
470
  }
472
471
  //#endregion
473
472
  //#region src/client/router.ts
@@ -639,9 +638,21 @@ function createRouter(deps) {
639
638
  function renderPayload(payload) {
640
639
  if (deps.renderRoot) deps.renderRoot(payload);
641
640
  }
642
- /** Update useParams() with route params from the server response. */
643
- function updateParams(params) {
644
- setCurrentParams(params ?? {});
641
+ /**
642
+ * Update navigation state (params + pathname) for the next render.
643
+ *
644
+ * Sets both the module-level fallback (for tests and SSR) and the
645
+ * navigation context state (read by renderRoot to wrap the element
646
+ * in NavigationProvider). The context update is atomic with the tree
647
+ * render — both are passed to reactRoot.render() in the same call.
648
+ */
649
+ function updateNavigationState(params, url) {
650
+ const resolvedParams = params ?? {};
651
+ setCurrentParams(resolvedParams);
652
+ setNavigationState({
653
+ params: resolvedParams,
654
+ pathname: url.startsWith("http") ? new URL(url).pathname : url.split("?")[0] || "/"
655
+ });
645
656
  }
646
657
  /** Apply head elements (title, meta tags) to the DOM if available. */
647
658
  function applyHead(elements) {
@@ -682,11 +693,8 @@ function createRouter(deps) {
682
693
  params: result.params
683
694
  });
684
695
  updateSegmentCache(result.segmentInfo);
685
- updateParams(result.params);
686
- flushSync(() => {
687
- renderPayload(result.payload);
688
- notifyParamsListeners();
689
- });
696
+ updateNavigationState(result.params, url);
697
+ renderPayload(result.payload);
690
698
  applyHead(result.headElements);
691
699
  window.dispatchEvent(new Event("timber:navigation-end"));
692
700
  afterPaint(() => {
@@ -717,11 +725,8 @@ function createRouter(deps) {
717
725
  params: result.params
718
726
  });
719
727
  updateSegmentCache(result.segmentInfo);
720
- updateParams(result.params);
721
- flushSync(() => {
722
- renderPayload(result.payload);
723
- notifyParamsListeners();
724
- });
728
+ updateNavigationState(result.params, currentUrl);
729
+ renderPayload(result.payload);
725
730
  applyHead(result.headElements);
726
731
  } finally {
727
732
  setPending(false);
@@ -730,11 +735,8 @@ function createRouter(deps) {
730
735
  async function handlePopState(url, scrollY = 0) {
731
736
  const entry = historyStack.get(url);
732
737
  if (entry && entry.payload !== null) {
733
- updateParams(entry.params);
734
- flushSync(() => {
735
- renderPayload(entry.payload);
736
- notifyParamsListeners();
737
- });
738
+ updateNavigationState(entry.params, url);
739
+ renderPayload(entry.payload);
738
740
  applyHead(entry.headElements);
739
741
  afterPaint(() => {
740
742
  deps.scrollTo(0, scrollY);
@@ -745,16 +747,13 @@ function createRouter(deps) {
745
747
  try {
746
748
  const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree());
747
749
  updateSegmentCache(result.segmentInfo);
748
- updateParams(result.params);
750
+ updateNavigationState(result.params, url);
749
751
  historyStack.push(url, {
750
752
  payload: result.payload,
751
753
  headElements: result.headElements,
752
754
  params: result.params
753
755
  });
754
- flushSync(() => {
755
- renderPayload(result.payload);
756
- notifyParamsListeners();
757
- });
756
+ renderPayload(result.payload);
758
757
  applyHead(result.headElements);
759
758
  afterPaint(() => {
760
759
  deps.scrollTo(0, scrollY);
@@ -924,31 +923,34 @@ function useRouter() {
924
923
  * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').
925
924
  * Updates when client-side navigation changes the URL.
926
925
  *
927
- * This is a thin wrapper over window.location.pathname, provided for
928
- * Next.js API compatibility (libraries like nuqs import usePathname
929
- * from next/navigation).
926
+ * On the client, reads from NavigationContext which is updated atomically
927
+ * with the RSC tree render. This replaces the previous useSyncExternalStore
928
+ * approach which only subscribed to popstate events — meaning usePathname()
929
+ * did NOT re-render on forward navigation (pushState). The context approach
930
+ * fixes this: pathname updates in the same render pass as the new tree.
930
931
  *
931
932
  * During SSR, reads the request pathname from the SSR ALS context
932
933
  * (populated by ssr-entry.ts) instead of window.location.
934
+ *
935
+ * Compatible with Next.js's `usePathname()` from `next/navigation`.
933
936
  */
934
- function getPathname() {
935
- if (typeof window !== "undefined") return window.location.pathname;
936
- return getSsrData()?.pathname ?? "/";
937
- }
938
- function getServerPathname() {
939
- return getSsrData()?.pathname ?? "/";
940
- }
941
- function subscribe$1(callback) {
942
- window.addEventListener("popstate", callback);
943
- return () => window.removeEventListener("popstate", callback);
944
- }
945
937
  /**
946
938
  * Read the current URL pathname.
947
939
  *
948
- * Compatible with Next.js's `usePathname()` from `next/navigation`.
940
+ * On the client, reads from NavigationContext (provided by
941
+ * NavigationProvider in renderRoot). During SSR, reads from the
942
+ * ALS-backed SSR data context. Falls back to window.location.pathname
943
+ * when called outside a React component (e.g., in tests).
949
944
  */
950
945
  function usePathname() {
951
- return useSyncExternalStore(subscribe$1, getPathname, getServerPathname);
946
+ try {
947
+ const navContext = useNavigationContext();
948
+ if (navContext !== null) return navContext.pathname;
949
+ } catch {}
950
+ const ssrData = getSsrData();
951
+ if (ssrData) return ssrData.pathname ?? "/";
952
+ if (typeof window !== "undefined") return window.location.pathname;
953
+ return "/";
952
954
  }
953
955
  //#endregion
954
956
  //#region src/client/use-search-params.ts
@@ -1247,6 +1249,6 @@ function useFormErrors(result) {
1247
1249
  };
1248
1250
  }
1249
1251
  //#endregion
1250
- export { HistoryStack, Link, LinkStatusContext, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setGlobalRouter, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, useParams, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1252
+ export { HistoryStack, Link, LinkStatusContext, NavigationContext, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getNavigationState, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, useParams, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1251
1253
 
1252
1254
  //# sourceMappingURL=index.js.map