@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
  2. package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
  5. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
  6. package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
  7. package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
  8. package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
  9. package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
  11. package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
  15. package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
  17. package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
  18. package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
  19. package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
  20. package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
  21. package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
  22. package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
  23. package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
  24. package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
  25. package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
  26. package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
  27. package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
  28. package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
  29. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  30. package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
  31. package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
  32. package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
  33. package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
  34. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
  35. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
  36. package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
  37. package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
  38. package/dist/_chunks/segment-context-D9_89u34.js +34 -0
  39. package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
  40. package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
  41. package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
  42. package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
  43. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
  44. package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
  45. package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
  46. package/dist/adapters/cloudflare-dev.js +1 -1
  47. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  48. package/dist/adapters/cloudflare.js +1 -1
  49. package/dist/adapters/nitro.d.ts +1 -1
  50. package/dist/adapters/nitro.d.ts.map +1 -1
  51. package/dist/adapters/nitro.js.map +1 -1
  52. package/dist/analyze/crawl-entry.js +2 -2
  53. package/dist/analyze/graph-command.js +2 -2
  54. package/dist/cache/index.js +1 -1
  55. package/dist/cache/singleflight.d.ts +2 -0
  56. package/dist/cache/singleflight.d.ts.map +1 -1
  57. package/dist/cli.js +2 -2
  58. package/dist/client/browser-entry/hydrate.d.ts +21 -15
  59. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  60. package/dist/client/browser-entry/index.d.ts +4 -3
  61. package/dist/client/browser-entry/index.d.ts.map +1 -1
  62. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  63. package/dist/client/browser-entry/router-init.d.ts +17 -1
  64. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  65. package/dist/client/error-boundary.js +1 -1
  66. package/dist/client/global-context.d.ts +15 -0
  67. package/dist/client/global-context.d.ts.map +1 -0
  68. package/dist/client/index.js +138 -35
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.d.ts +0 -1
  71. package/dist/client/internal.d.ts.map +1 -1
  72. package/dist/client/internal.js +206 -55
  73. package/dist/client/internal.js.map +1 -1
  74. package/dist/client/link.d.ts.map +1 -1
  75. package/dist/client/location-search.d.ts +12 -0
  76. package/dist/client/location-search.d.ts.map +1 -0
  77. package/dist/client/navigation-api.d.ts.map +1 -1
  78. package/dist/client/navigation-commit.d.ts +18 -0
  79. package/dist/client/navigation-commit.d.ts.map +1 -1
  80. package/dist/client/navigation-context.d.ts +13 -11
  81. package/dist/client/navigation-context.d.ts.map +1 -1
  82. package/dist/client/navigation-root.d.ts +47 -108
  83. package/dist/client/navigation-root.d.ts.map +1 -1
  84. package/dist/client/navigation-transition.d.ts +136 -0
  85. package/dist/client/navigation-transition.d.ts.map +1 -0
  86. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  87. package/dist/client/params-context.d.ts +4 -5
  88. package/dist/client/params-context.d.ts.map +1 -1
  89. package/dist/client/react-root.d.ts +44 -0
  90. package/dist/client/react-root.d.ts.map +1 -0
  91. package/dist/client/router-pipeline.d.ts +2 -2
  92. package/dist/client/router-pipeline.d.ts.map +1 -1
  93. package/dist/client/router-types.d.ts +12 -2
  94. package/dist/client/router-types.d.ts.map +1 -1
  95. package/dist/client/router.d.ts.map +1 -1
  96. package/dist/client/segment-cache.d.ts +39 -0
  97. package/dist/client/segment-cache.d.ts.map +1 -1
  98. package/dist/client/segment-context.d.ts.map +1 -1
  99. package/dist/client/segment-outlet.d.ts +25 -14
  100. package/dist/client/segment-outlet.d.ts.map +1 -1
  101. package/dist/client/segment-update-context.d.ts +3 -9
  102. package/dist/client/segment-update-context.d.ts.map +1 -1
  103. package/dist/client/slot-content-cache-context.d.ts +35 -0
  104. package/dist/client/slot-content-cache-context.d.ts.map +1 -0
  105. package/dist/client/ssr-data.d.ts +8 -2
  106. package/dist/client/ssr-data.d.ts.map +1 -1
  107. package/dist/client/state.d.ts +0 -15
  108. package/dist/client/state.d.ts.map +1 -1
  109. package/dist/client/use-pathname.d.ts +13 -11
  110. package/dist/client/use-pathname.d.ts.map +1 -1
  111. package/dist/client/use-search-params.d.ts +13 -13
  112. package/dist/client/use-search-params.d.ts.map +1 -1
  113. package/dist/client/use-segment-params.d.ts +18 -68
  114. package/dist/client/use-segment-params.d.ts.map +1 -1
  115. package/dist/config-types.d.ts +17 -0
  116. package/dist/config-types.d.ts.map +1 -1
  117. package/dist/config-validation.d.ts.map +1 -1
  118. package/dist/cookies/index.js +1 -1
  119. package/dist/dev-tools/holding-server.d.ts +15 -10
  120. package/dist/dev-tools/holding-server.d.ts.map +1 -1
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +44 -43
  123. package/dist/index.js.map +1 -1
  124. package/dist/plugins/dev-server.d.ts.map +1 -1
  125. package/dist/plugins/entries.d.ts.map +1 -1
  126. package/dist/plugins/shims.d.ts.map +1 -1
  127. package/dist/plugins/static-build.d.ts +2 -2
  128. package/dist/plugins/static-build.d.ts.map +1 -1
  129. package/dist/routing/codegen-write.d.ts.map +1 -1
  130. package/dist/routing/index.js +2 -2
  131. package/dist/routing/interception-overlap.d.ts +35 -0
  132. package/dist/routing/interception-overlap.d.ts.map +1 -0
  133. package/dist/routing/interception.d.ts.map +1 -1
  134. package/dist/rsc-runtime/ssr.d.ts +3 -1
  135. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  136. package/dist/server/als-registry.d.ts +6 -0
  137. package/dist/server/als-registry.d.ts.map +1 -1
  138. package/dist/server/csp-nonce.d.ts +45 -0
  139. package/dist/server/csp-nonce.d.ts.map +1 -0
  140. package/dist/server/default-status-page.d.ts.map +1 -1
  141. package/dist/server/deny-renderer.d.ts.map +1 -1
  142. package/dist/server/flight-scripts.d.ts +5 -2
  143. package/dist/server/flight-scripts.d.ts.map +1 -1
  144. package/dist/server/html-injector-core.d.ts +17 -2
  145. package/dist/server/html-injector-core.d.ts.map +1 -1
  146. package/dist/server/html-injectors.d.ts +3 -2
  147. package/dist/server/html-injectors.d.ts.map +1 -1
  148. package/dist/server/index.js +2 -2
  149. package/dist/server/internal.js +86 -37
  150. package/dist/server/internal.js.map +1 -1
  151. package/dist/server/metadata-render.d.ts.map +1 -1
  152. package/dist/server/node-stream-transforms.d.ts +3 -17
  153. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  154. package/dist/server/nuqs-ssr-provider.d.ts +7 -3
  155. package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
  156. package/dist/server/pipeline-phases.d.ts.map +1 -1
  157. package/dist/server/prebuilt/key-discipline.d.ts +32 -3
  158. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
  159. package/dist/server/primitives.d.ts.map +1 -1
  160. package/dist/server/render-utils.d.ts +4 -3
  161. package/dist/server/render-utils.d.ts.map +1 -1
  162. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  164. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  166. package/dist/server/ssr-bridge-types.d.ts +22 -2
  167. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  168. package/dist/server/ssr-entry.d.ts.map +1 -1
  169. package/dist/server/ssr-render.d.ts +5 -1
  170. package/dist/server/ssr-render.d.ts.map +1 -1
  171. package/dist/server/ssr-wrappers.d.ts +59 -27
  172. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  173. package/dist/server/types.d.ts +10 -0
  174. package/dist/server/types.d.ts.map +1 -1
  175. package/dist/shims/navigation-rsc.d.ts +21 -0
  176. package/dist/shims/navigation-rsc.d.ts.map +1 -0
  177. package/docs/api/30-api-server.mdx +1 -0
  178. package/docs/api/35-api-typescript.mdx +4 -83
  179. package/docs/learn/03-fetching-data.mdx +1 -1
  180. package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
  181. package/docs/learn/05-the-flush-point.mdx +175 -0
  182. package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
  183. package/docs/learn/07-typed-routes.mdx +25 -49
  184. package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
  185. package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
  186. package/package.json +3 -3
  187. package/src/adapters/nitro.ts +7 -7
  188. package/src/cache/singleflight.ts +5 -0
  189. package/src/client/browser-entry/hydrate.ts +54 -104
  190. package/src/client/browser-entry/index.ts +16 -6
  191. package/src/client/browser-entry/post-hydration.ts +3 -2
  192. package/src/client/browser-entry/router-init.ts +84 -33
  193. package/src/client/global-context.ts +31 -0
  194. package/src/client/internal.ts +1 -2
  195. package/src/client/link.tsx +18 -18
  196. package/src/client/location-search.ts +15 -0
  197. package/src/client/navigation-api.ts +4 -2
  198. package/src/client/navigation-commit.ts +48 -2
  199. package/src/client/navigation-context.ts +25 -37
  200. package/src/client/navigation-root.tsx +55 -411
  201. package/src/client/navigation-transition.ts +278 -0
  202. package/src/client/nuqs-adapter.tsx +4 -5
  203. package/src/client/params-context.ts +13 -18
  204. package/src/client/react-root.ts +72 -0
  205. package/src/client/router-lifecycle.ts +1 -1
  206. package/src/client/router-pipeline.ts +96 -22
  207. package/src/client/router-types.ts +12 -2
  208. package/src/client/router.ts +48 -36
  209. package/src/client/segment-cache.ts +70 -2
  210. package/src/client/segment-context.ts +7 -4
  211. package/src/client/segment-outlet.tsx +41 -86
  212. package/src/client/segment-update-context.ts +7 -26
  213. package/src/client/slot-content-cache-context.ts +43 -0
  214. package/src/client/ssr-data.ts +8 -2
  215. package/src/client/state.ts +0 -26
  216. package/src/client/use-pathname.ts +21 -31
  217. package/src/client/use-search-params.ts +31 -29
  218. package/src/client/use-segment-params.ts +27 -126
  219. package/src/config-types.ts +17 -0
  220. package/src/config-validation.ts +17 -0
  221. package/src/dev-tools/holding-server.ts +23 -12
  222. package/src/index.ts +26 -11
  223. package/src/plugins/dev-server.ts +9 -12
  224. package/src/plugins/entries.ts +3 -0
  225. package/src/plugins/shims.ts +8 -7
  226. package/src/plugins/static-build.ts +9 -5
  227. package/src/react-canary.d.ts +2 -0
  228. package/src/routing/codegen-write.ts +2 -0
  229. package/src/routing/interception-overlap.ts +141 -0
  230. package/src/routing/interception.ts +118 -5
  231. package/src/rsc-runtime/ssr.ts +3 -2
  232. package/src/server/als-registry.ts +6 -0
  233. package/src/server/csp-nonce.ts +70 -0
  234. package/src/server/default-status-page.ts +1 -0
  235. package/src/server/deny-renderer.ts +7 -3
  236. package/src/server/flight-scripts.ts +9 -4
  237. package/src/server/html-injector-core.ts +26 -9
  238. package/src/server/html-injectors.ts +8 -8
  239. package/src/server/metadata-render.ts +26 -4
  240. package/src/server/node-stream-transforms.ts +7 -20
  241. package/src/server/nuqs-ssr-provider.tsx +8 -7
  242. package/src/server/pipeline-phases.ts +5 -0
  243. package/src/server/prebuilt/key-discipline.ts +82 -13
  244. package/src/server/prebuilt-runtime.ts +2 -2
  245. package/src/server/primitives.ts +4 -4
  246. package/src/server/render-utils.ts +8 -4
  247. package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
  248. package/src/server/rsc-entry/error-renderer.ts +5 -2
  249. package/src/server/rsc-entry/index.ts +8 -0
  250. package/src/server/rsc-entry/ssr-renderer.ts +11 -4
  251. package/src/server/ssr-bridge-types.ts +22 -2
  252. package/src/server/ssr-entry.ts +35 -28
  253. package/src/server/ssr-render.ts +13 -4
  254. package/src/server/ssr-wrappers.tsx +81 -61
  255. package/src/server/types.ts +10 -0
  256. package/src/shared/slot-params.ts +3 -4
  257. package/src/shims/navigation-rsc.ts +47 -0
  258. package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
  259. package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
  260. package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
  261. package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
  262. package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
  263. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
  264. package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
  265. package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
  266. package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
  267. package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
  268. package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
  269. package/docs/learn/04-loading-states.mdx +0 -67
  270. package/docs/learn/04b-the-flush-point.mdx +0 -115
  271. package/docs/learn/12-client-navigation.mdx +0 -176
  272. package/docs/learn/13-configuration.mdx +0 -166
  273. package/docs/more/01-advanced-routing.mdx +0 -344
  274. package/docs/more/02-advanced-forms.mdx +0 -137
  275. package/docs/more/03-coming-from-nextjs.mdx +0 -186
  276. package/docs/more/04-metadata-and-fonts.mdx +0 -193
  277. package/docs/more/04b-mdx.mdx +0 -229
  278. package/docs/more/05-content-collections.mdx +0 -90
  279. package/docs/more/06-instrumentation.mdx +0 -214
  280. package/docs/more/07-security.mdx +0 -129
  281. package/docs/more/08-developer-experience.mdx +0 -134
  282. package/docs/more/40-why-timber.mdx +0 -50
  283. package/docs/more/41-timber-vs-nextjs.mdx +0 -81
  284. package/docs/more/42-timber-vs-others.mdx +0 -68
  285. package/docs/more/50-ai-agent-instructions.mdx +0 -171
  286. /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
  287. /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
  288. /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
  289. /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-segment-params-DzTBpkvj.js","names":[],"sources":["../../src/client/use-pending-navigation.ts","../../src/client/navigation-context.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx","../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["import { useSyncExternalStore } from 'react';\nimport { getRouterOrNull } from './router-ref.ts';\n\nfunction subscribe(onStoreChange: () => void): () => void {\n const router = getRouterOrNull();\n if (!router) return () => {};\n return router.onPendingChange(onStoreChange);\n}\n\nfunction getSnapshot(): boolean {\n const router = getRouterOrNull();\n return router ? router.isPending() : false;\n}\n\nconst getServerSnapshot = getSnapshot;\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * Reads from the router's external pending store via useSyncExternalStore.\n * Only components that call this hook re-render when pending state\n * changes — no full-tree re-render.\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 return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current pathname and search, updated atomically with the RSC\n * tree on each navigation. This replaces the previous useSyncExternalStore\n * approach for usePathname() and useSearchParams(), which suffered from a\n * timing gap: the new tree could commit before the external store\n * re-renders fired, causing a frame where both old and new active states\n * were visible simultaneously.\n *\n * Segment params are NOT here — see the note on NavigationState below.\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 (usePathname, useSearchParams) 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';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\n/**\n * What the browser knows about the current location.\n *\n * Params are deliberately absent: they are a property of the tree the server\n * rendered, not of the address bar, and they now travel inside the payload as\n * the payload root. Keeping a copy here meant the router had to thread\n * the record from a response header into context on every path — navigation,\n * traversal, prefetch, revalidation — and each of those was a place to drop it\n * (TIM-1294).\n */\nexport interface NavigationState {\n pathname: string;\n search: 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\n// Symbol keys for globalThis storage — prevents collisions with user code\nconst NAV_CTX_KEY = Symbol.for('__timber_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 usePathname() and useSearchParams().\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 // eslint-disable-next-line rules-of-hooks -- conditional on environment, not render path\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: { pathname: '/', search: '' } };\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 state lives in the router, not here\n// ---------------------------------------------------------------------------\n\n/**\n * There was a second React context here — `PendingNavigationContext`, holding\n * the in-flight navigation URL, provided by `NavigationRoot` out of a\n * `useState` — plus a `usePendingNavigationUrl()` reader for it. Both are gone\n * (TIM-1307).\n *\n * Nothing read them. `usePendingNavigation()` (use-pending-navigation.ts) and\n * `TopLoader` both subscribe to the router's external pending store via\n * `useSyncExternalStore`: sync priority, immune to transition entanglement,\n * and cleared by `runNavigation`'s supersession-guarded `finally` (TIM-1034).\n * The context was a parallel representation of the same fact that no consumer\n * ever migrated to, and one that could not even agree with the store — a\n * navigation superseded by a cached popstate replay left its URL set until the\n * next full navigation, because the staleness guard skipped the clear and no\n * other path touched it.\n *\n * One representation, and it is the router's. See\n * design/19-client-navigation.md §\"How Pending State Works\".\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 { usePendingNavigation } from './use-pending-navigation.ts';\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 the router's external store, via\n * usePendingNavigation() — the one pending representation (TIM-1307).\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 // Read pending state from the router's external store via\n // useSyncExternalStore (inside usePendingNavigation). Only this\n // component re-renders when pending changes — no full-tree re-render.\n const isPending = usePendingNavigation();\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 * the new tree resolves, instead of hiding it behind a Suspense fallback.\n *\n * The navigation's async work runs OUTSIDE the transition scope and every\n * state update it schedules gets its own synchronous `startTransition` — a\n * transition scope does not survive an `await`, and an async callback that\n * returns a thenable defers the commit past the promise callers await. See\n * the long comment on `_navigateTransition` (TIM-1306).\n *\n * This component holds no pending state. The `TopLoader` it renders and the\n * public `usePendingNavigation()` both subscribe to the router's external\n * pending store; NavigationRoot's own `pendingUrl` was a second, unread\n * representation of the same fact and is gone (TIM-1307).\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 {\n createElement,\n Fragment,\n startTransition,\n useLayoutEffect,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.tsx';\n\n// ─── Transition Result ──────────────────────────────────────────\n\n/**\n * What a navigation's `perform()` hands back to the transition.\n *\n * Declared once and shared by every layer that passes it along — the router's\n * `RouterDeps.navigateTransition` imports it too — so a field cannot be added\n * to the producer and dropped by the adapter in between. `E` is the element\n * type: `ReactNode` here, `unknown` in the router, which never touches it.\n */\nexport interface TransitionResult<E = ReactNode> {\n /** The wrapped tree to render. */\n element: E;\n /** Resolves when the Flight stream finishes decoding, or null. */\n decodePromise: Promise<void> | null;\n /**\n * Publishes the navigation's state — segment cache, pathname, address bar,\n * history stack, and the client's record of the mounted tree — and\n * announces it to listeners outside React.\n *\n * Run when React commits this tree, not when the tree is handed over: a\n * tree React is still waiting on, or one a later navigation replaces first,\n * has been given to React without being on screen. A superseded navigation\n * never runs it and leaves every one of those consumers describing the\n * route still on screen (TIM-1301).\n */\n commit: () => void;\n}\n\n/**\n * The tree NavigationRoot holds in state, and the state publish that belongs\n * to it. They travel as one object so React's commit of the tree is the event\n * that publishes — see the effect in NavigationRoot.\n */\ninterface RenderedTree {\n element: ReactNode;\n publish: (() => void) | null;\n}\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\n/**\n * `waiters` are woken on every bump so an in-flight navigation can stop\n * waiting on its own payload the moment it is superseded — see\n * `settleOnDecodeOrSupersession`.\n */\ninterface TransitionCounter {\n id: number;\n waiters: Set<() => void>;\n}\n\nfunction getTransitionCounter(): TransitionCounter {\n const g = globalThis as Record<symbol, unknown>;\n const existing = g[NAV_TRANSITION_KEY] as Partial<TransitionCounter> | undefined;\n if (!existing) {\n const created: TransitionCounter = { id: 0, waiters: new Set() };\n g[NAV_TRANSITION_KEY] = created;\n return created;\n }\n // A duplicated copy of this module may have created the singleton before\n // `waiters` existed. The object is shared across chunks, so fill it in\n // rather than replacing it — replacing would strand the other copy's id.\n existing.waiters ??= new Set();\n return existing as TransitionCounter;\n}\n\n/** Bump the counter and wake everything waiting on an older transition. */\nfunction bumpTransitionCounter(): number {\n const counter = getTransitionCounter();\n counter.id += 1;\n for (const wake of [...counter.waiters]) wake();\n return counter.id;\n}\n\n/**\n * Invalidate all in-flight navigation transitions. Any navigateTransition()\n * call whose perform() has not yet committed will reject with AbortError\n * instead of committing its element.\n *\n * Called by the router when a render supersedes in-flight navigations\n * WITHOUT going through navigateTransition() — the cached popstate replay\n * renders via transitionRender(), which doesn't bump the counter, so a\n * stale forward navigation's setElement would otherwise pass the\n * `counter.id !== transId` guard and commit the forward page over the\n * replayed back page (TIM-1022).\n */\nexport function supersedeNavigationTransitions(): void {\n bumpTransitionCounter();\n}\n\n/**\n * Wait for the payload to finish decoding, OR for this transition to be\n * superseded — whichever happens first.\n *\n * A superseded navigation must stop waiting on its own stream. The stream is\n * deliberately NOT aborted once its tree has been handed to React (the tree\n * may be on screen with boundaries still feeding from it — see\n * `handedOffNavAbort` in `client/router.ts`), so there is nothing left to make\n * `decodePromise` settle promptly. Awaiting it bare would keep the loser's\n * `router.navigate()` promise pending for the rest of the stream — and\n * forever if it stalls — which is what `<Link>`'s `isPending` is timed\n * against, so the losing link would sit spinning while the winner loaded\n * (codex on #1004).\n *\n * A decode *failure* still propagates: it is a real error for this\n * navigation, and the caller's recovery is timed against it.\n */\nfunction settleOnDecodeOrSupersession(\n decodePromise: Promise<void>,\n counter: TransitionCounter,\n transId: number\n): Promise<void> {\n if (counter.id !== transId) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const stopWaiting = (): void => {\n counter.waiters.delete(wake);\n };\n const wake = (): void => {\n if (counter.id !== transId) {\n stopWaiting();\n resolve();\n }\n };\n counter.waiters.add(wake);\n decodePromise.then(\n () => {\n stopWaiting();\n resolve();\n },\n (error: unknown) => {\n stopWaiting();\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n );\n });\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 *\n * Runs the fetch OUTSIDE any transition scope and wraps only the state update\n * that hands the resulting tree to React — describing this as \"a full\n * navigation in a single startTransition\" is the shape TIM-1306 removed.\n */\nlet _navigateTransition:\n | ((url: string, perform: () => Promise<TransitionResult>) => Promise<void>)\n | null = null;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root wrapper component that enables transition-based rendering.\n *\n * Renders the TopLoader alongside the tree it holds in state. Neither adds a\n * DOM element on the hydration path, so the tree matches the server HTML.\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 [rendered, setRendered] = useState<RenderedTree>({ element: initial, publish: null });\n\n // Publish the navigation's state when React commits its tree — not when the\n // tree is handed over. A tree React is still waiting on, or one a later\n // navigation replaces before React renders it, has been *given* to React\n // without being on screen; publishing then describes a route the user never\n // saw, which is the defect this ordering exists to prevent (TIM-1301).\n //\n // A **layout** effect, so the publish lands before the browser paints and,\n // more importantly, before every descendant's passive effect. A destination\n // component that navigates from a mount effect — a redirect guard, a\n // `refresh()` on mount — would otherwise start its navigation while the\n // segment cache, address bar and pathname still described the departing\n // route, and send an X-Timber-State-Tree for a tree that is no longer\n // mounted (codex on #998).\n //\n // Descendant *layout* effects still run first: React runs layout effects\n // child-before-parent, and the only way to precede them would be a fiber\n // rendered as an earlier sibling of the payload. That is not free here —\n // `server/ssr-wrappers.tsx` mirrors this component's fiber shape so `useId`\n // agrees across hydration, and an extra sibling shifts every id in the\n // payload subtree. A layout effect that navigates on mount is the price.\n //\n // Keyed on the tree object rather than on the effect run. A publish moves\n // the address bar, which is not idempotent, so any second invocation for the\n // same tree — an effect re-run React is entitled to perform — must be a\n // no-op rather than a second history entry.\n const publishedRef = useRef<RenderedTree | null>(null);\n useLayoutEffect(() => {\n if (publishedRef.current === rendered) return;\n publishedRef.current = rendered;\n rendered.publish?.();\n }, [rendered]);\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` — pending state lives in the router's\n // external store, which TopLoader and usePendingNavigation() subscribe to.\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 // Non-navigation render (revalidation, popstate cached replay). Both publish\n // synchronously in the router before calling this — neither has an in-flight\n // window in which it could be superseded — so there is nothing to publish\n // on commit.\n _transitionRender = (newElement: ReactNode) => {\n startTransition(() => {\n setRendered({ element: newElement, publish: null });\n });\n };\n\n // Full navigation transition.\n //\n // The async work runs OUTSIDE any transition scope, and each state update is\n // its own synchronous `startTransition`. Two React behaviours force that\n // shape; both were measured on React 19.2.7 (tests/navigation-transition-\n // suspense.test.ts pins the observable half).\n //\n // 1. A transition scope does not survive an `await`. `startTransition`\n // restores the previous scope in its `finally`, which for an async\n // callback runs when that callback RETURNS — i.e. at its first `await`.\n // So an update scheduled past an await inside `startTransition(async …)`\n // is an ordinary urgent update:\n //\n // startTransition(() => setEl(x)) -> 'HOME' held\n // startTransition(async () => { await p; setEl(x) }) -> fallback shown\n //\n // Left uncorrected, every navigation whose new tree suspends replaces the\n // visible page with a Suspense fallback — the exact thing this component\n // exists to prevent (TIM-1306). React documents the caveat under\n // `startTransition`: updates after an await need their own transition.\n //\n // 2. Re-wrapping *inside* the async callback is not enough. Returning a\n // thenable from `startTransition` hands it to `ReactSharedInternals.S`,\n // which calls react-dom's `entangleAsyncAction`. That opens an action\n // scope: `currentEntangledLane` collects EVERY transition update\n // scheduled while the scope is open — including one from a nested,\n // fully synchronous `startTransition` — and rendering that lane throws\n // `currentEntangledActionThenable`, suspending until the action settles.\n // Which is after the promise `navigateTransition` hands back, so a caller\n // that awaits a navigation and then reads the DOM sees the departing\n // page, and the TIM-1301 publish (a layout effect on the commit) is just\n // as late. (Not `ReactSharedInternals.asyncTransitions` — that counter is\n // write-only in 19.2.7.)\n //\n // No async action is created here, so every navigation commits as soon as\n // React can render its destination — where TIM-1301 put it. That\n // now holds for ALL of them. It used not to hold for `<Link>`, which wrapped\n // `router.navigate()` in its own `useTransition`: that action scope\n // entangled this component's updates just the same, so a Link navigation\n // committed only once the payload had finished decoding, and with it\n // `pushState`, the segment cache and `timber:navigation-end`. Link now calls\n // `router.navigate()` outside any transition and tracks its own `isPending`\n // with a plain `useState` (TIM-1307).\n //\n // Nothing here may reopen an action scope: no `startTransition` callback in\n // this path may return a thenable, and no caller may invoke this from inside\n // a React action scope. That is a rule about `startTransition`, not about\n // `perform` — `perform` is async by contract and is awaited OUTSIDE any\n // transition scope, which is exactly why it is safe.\n //\n // Standalone `startTransition` rather than `useTransition`'s is still\n // deliberate — see the TIM-625 note above; each navigation needs an\n // independent lane.\n //\n // `url` is unused: it named the pending state this component used to hold,\n // and the router already publishes the same URL to its own pending store\n // before calling here. It stays in the signature because the router's\n // `RouterDeps.navigateTransition` adapter passes it and the argument reads\n // at the call site.\n _navigateTransition = (_url: string, perform: () => Promise<TransitionResult>) => {\n // Increment the transition counter SYNCHRONOUSLY (before any await). Each\n // call gets a unique transId; the counter is the same globalThis\n // singleton, so a newer call always has a higher id.\n const counter = getTransitionCounter();\n const transId = bumpTransitionCounter();\n\n const superseded = () => new DOMException('Navigation superseded', 'AbortError');\n\n return (async () => {\n const { element, decodePromise, commit } = await perform();\n if (counter.id !== transId) {\n decodePromise?.catch(() => {});\n throw superseded();\n }\n // Hand the tree over with its commit attached. The effect above runs\n // it if and when React commits this tree, so a navigation that is\n // superseded while its payload streams publishes nothing (TIM-1301).\n //\n // This is THE update that must not hide the departing page (TIM-1306).\n startTransition(() => {\n setRendered({ element, publish: commit });\n });\n // React may commit the tree before this settles — that is the point:\n // the destination reveals as React is able to render it\n // rather than waiting for the whole Flight stream. The await is here so\n // the promise this function hands back still means \"the payload is\n // decoded\", which is what the router's scroll restoration, the\n // Navigation API deferred and `<Link>`'s `isPending` are timed against.\n //\n // ...unless this navigation loses first, in which case it stops waiting\n // on a stream that is no longer its business. See\n // `settleOnDecodeOrSupersession`.\n if (decodePromise) await settleOnDecodeOrSupersession(decodePromise, counter, transId);\n if (counter.id !== transId) throw superseded();\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 useState,\n // useRef and useLayoutEffect above). React requires the same hooks\n // to run on every render. If we threw before hooks, React would see\n // 0 hooks on the re-render vs 3 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. It subscribes to the router's\n // pending store itself, so it takes no props from here beyond its config,\n // and only it re-renders when a navigation starts or ends. Rendered only\n // when not explicitly disabled via config.\n //\n // This Fragment is a FORK — two children — so it advances React's tree\n // context and shifts every `useId` below it. `server/ssr-wrappers.tsx`\n // mirrors it for exactly that reason; a single-child wrapper would not need\n // a mirror. See tests/ssr-wrapper-parity.test.ts.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n if (!showTopLoader) return rendered.element;\n return createElement(\n Fragment,\n null,\n createElement(TopLoader, { config: topLoaderConfig }),\n rendered.element\n );\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, handing its tree to React in a transition.\n *\n * The `perform` callback fetches the RSC payload, updates router state, and\n * returns the wrapped React element. `perform` is async by contract and runs\n * OUTSIDE any transition scope — only the `setRendered` that hands its result\n * over is wrapped, in a synchronous `startTransition`.\n *\n * Do not call this from inside a React action scope, and never let a\n * `startTransition` callback in this path return a thenable: either opens an\n * action scope that entangles the update and defers the commit until the\n * action settles (TIM-1306, TIM-1307).\n *\n * Returns a Promise that resolves when the async work completes — the payload\n * is fetched, decoded, and handed to React. It does **not** wait for React to\n * commit the tree, so the state that publishes on that commit (address bar,\n * segment cache, history entry, pathname) may still be a beat behind when it\n * resolves. Anything that needs the destination to be current should listen\n * for `timber:navigation-end`, which is dispatched by the publish itself.\n *\n * Awaiting the publish here was tried and rejected: it makes the promise\n * depend on a React commit, which never arrives if the root unmounts\n * mid-navigation, and deadlocks any caller that awaits a navigation inside\n * `act()`.\n *\n * Used for: navigate(), refresh(), popstate with fetch.\n */\nexport function navigateTransition(\n url: string,\n perform: () => Promise<TransitionResult>\n): Promise<void> {\n if (_navigateTransition) {\n return _navigateTransition(url, perform);\n }\n // Fallback: no NavigationRoot mounted (shouldn't happen in production).\n // Nothing can supersede a transition that does not exist, so the commit\n // runs unconditionally.\n return perform().then((result) => result.commit());\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 (_url: string, perform: () => Promise<TransitionResult>) => {\n const { element, commit } = await perform();\n commit();\n mountOnce(element);\n };\n}\n","/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { _setCurrentParams, _setCurrentSlotParams } from './state.ts';\nimport { toNullProtoRecord, type CoercedParams } from '../shared/param-value.ts';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call silently falls back to the module snapshot.\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from every entry a Server Component imports, so the named form crashed\n * those entries outright (codex, PR #992; originally reproduced against\n * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —\n * the hazard is unchanged for the entries that remain).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null when no provider is\n * above the caller — a component rendered outside a timber route, a\n * `useSegmentParams()` call from outside React entirely, or any component\n * during SSR (where the params reach the hook through the ALS-backed SSR data\n * context instead, and there is no client-owned tree to hold a provider).\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: CoercedParams;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * The module-level snapshot in `state.ts` is written during render rather\n * than in an effect. It is the fallback path for `useSegmentParams()` called\n * outside a component (tests, module scope), and an effect would leave that\n * path reading the *previous* route's params for the whole commit — the\n * window in which a navigation's components actually run. Writing during\n * render is safe here because the value is derived entirely from props: a\n * double-invoked render in StrictMode writes the same record twice.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n // Keep the out-of-component fallback in step with the tree being rendered.\n _setCurrentParams(value.params);\n _setCurrentSlotParams(value.slotParams);\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * 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 ParamsContext, published\n * by the one provider the client renders above the merge point\n * (`PayloadRoot`). Params update atomically with the tree because they travel\n * on the same payload root — there is no separate channel that could be\n * seeded a render early or late (TIM-1294, TIM-1297).\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 { CoercedParams } from '../shared/param-value.ts';\nimport type { Routes } from '../index.ts';\nimport { getSsrData } from './ssr-data.ts';\nimport {\n currentParams,\n currentSlotParams,\n _setCurrentParams,\n _setCurrentSlotParams,\n paramsListeners,\n} from './state.ts';\nimport { resolveSegmentParams, type SlotParamsRecord } from '../shared/slot-params.ts';\nimport { useParamsContext } from './params-context.ts';\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(): CoercedParams {\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: CoercedParams): void {\n _setCurrentParams(params);\n}\n\n/**\n * Set the per-slot params snapshot in the module-level store.\n *\n * Paired with `setCurrentParams`: the router calls both on every navigation,\n * including with `null` when a response carries no slot params, so a slot's\n * params from the *previous* route cannot be read on the next one. Fill and\n * serve are paired; so are fill and clear. See TIM-1285.\n */\nexport function setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n _setCurrentSlotParams(slotParams);\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 ParamsContext, published by `PayloadRoot` above\n * everything the navigation renders. Params update atomically with the RSC\n * 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 segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams {\n // Try the client-owned provider first. It sits above everything a navigation\n // renders, so any component on the page — initial document, full navigation,\n // or a layout the server skipped — has one above it. Absent during SSR,\n // where the ALS path below is the answer. When called outside a React\n // component, useContext throws — caught below.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const paramsContext = useParamsContext();\n if (paramsContext !== null) {\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\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 const ssrData = getSsrData();\n if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);\n return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);\n}\n"],"mappings":";;;;;;AAGA,SAAS,UAAU,eAAuC;CACxD,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,aAAa,CAAC;CAC3B,OAAO,OAAO,gBAAgB,aAAa;AAC7C;AAEA,SAAS,cAAuB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,OAAO,UAAU,IAAI;AACvC;AAEA,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuB1B,SAAgB,uBAAgC;CAC9C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC8CA,IAAM,cAAc,OAAO,IAAI,kBAAkB;AAEjD,SAAS,uBAAwE;CAC/E,MAAM,WAAY,WAAuC;CAGzD,IAAI,aAAa,KAAA,GAAW,OAAO;CAEnC,IAAI,OAAO,MAAM,kBAAkB,YAAY;EAC7C,MAAM,MAAM,MAAM,cAAsC,IAAI;EAC5D,WAAwC,eAAe;EACvD,OAAO;CACT;AAEF;;;;;;AAOA,SAAgB,uBAA+C;CAC7D,MAAM,MAAM,qBAAmB;CAC/B,IAAI,CAAC,KAAK,OAAO;CAEjB,IAAI,OAAO,MAAM,eAAe,YAAY,OAAO;CAEnD,OAAO,MAAM,WAAW,GAAG;AAC7B;;;;;;;AAiBA,SAAgB,mBAAmB,EACjC,OACA,YAC8C;CAC9C,MAAM,MAAM,qBAAmB;CAC/B,IAAI,CAAC,KAEH,OAAO;CAET,OAAO,cAAc,IAAI,UAAU,EAAE,MAAM,GAAG,QAAQ;AACxD;;;;;;;;;;;;;;AAmBA,IAAM,gBAAgB,OAAO,IAAI,oBAAoB;AAErD,SAAS,oBAAkD;CACzD,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,gBACL,EAAE,iBAAiB,EAAE,SAAS;EAAE,UAAU;EAAK,QAAQ;CAAG,EAAE;CAE9D,OAAO,EAAE;AACX;AAEA,SAAgB,mBAAmB,OAA8B;CAC/D,kBAAkB,CAAC,CAAC,UAAU;AAChC;AAEA,SAAgB,qBAAsC;CACpD,OAAO,kBAAkB,CAAC,CAAC;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AE3EA,IAAM,qBAAqB,OAAO,IAAI,iCAAiC;AAYvE,SAAS,uBAA0C;CACjD,MAAM,IAAI;CACV,MAAM,WAAW,EAAE;CACnB,IAAI,CAAC,UAAU;EACb,MAAM,UAA6B;GAAE,IAAI;GAAG,yBAAS,IAAI,IAAI;EAAE;EAC/D,EAAE,sBAAsB;EACxB,OAAO;CACT;CAIA,SAAS,4BAAY,IAAI,IAAI;CAC7B,OAAO;AACT;;AAGA,SAAS,wBAAgC;CACvC,MAAM,UAAU,qBAAqB;CACrC,QAAQ,MAAM;CACd,KAAK,MAAM,QAAQ,CAAC,GAAG,QAAQ,OAAO,GAAG,KAAK;CAC9C,OAAO,QAAQ;AACjB;;;;;;;;;;;;;AAcA,SAAgB,iCAAuC;CACrD,sBAAsB;AACxB;;;;;;;;;;;;;;;;;AAmEA,IAAM,eAAe,OAAO,IAAI,0BAA0B;AAE1D,SAAS,kBAAsC;CAC7C,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,eACL,EAAE,gBAAgB,EAAE,OAAO,MAAM;CAEnC,OAAO,EAAE;AACX;;;;;;;AAQA,SAAgB,kBAAkB,OAAsB;CACtD,gBAAgB,CAAC,CAAC,QAAQ;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjKA,IAAM,iBAAiB,OAAO,IAAI,qBAAqB;AAEvD,SAAS,qBAA+D;CACtE,MAAM,QAAQ;CACd,MAAM,WAAW,MAAM;CACvB,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,IAAI,OAAO,MAAM,kBAAkB,YAGjC;CAEF,MAAM,MAAM,MAAM,cAAyC,IAAI;CAC/D,MAAM,kBAAkB;CACxB,OAAO;AACT;AAEA,IAAM,gBAAgB,mBAAmB;;;;;;;;AASzC,SAAgB,mBAA8C;CAC5D,OAAO,MAAM,WAAW,aAAa;AACvC;;;;;;;;;;;;;;;;;;ACbA,SAAgB,iBAAiB,QAA6B;CAC5D,kBAAkB,MAAM;AAC1B;;;;;;;;;AAUA,SAAgB,qBAAqB,YAA2C;CAC9E,sBAAsB,UAAU;AAClC;AA4CA,SAAgB,iBAAiB,aAAqC;CAMpE,IAAI;EAEF,MAAM,gBAAgB,iBAAiB;EACvC,IAAI,kBAAkB,MACpB,OAAO,qBAAqB,cAAc,QAAQ,cAAc,YAAY,WAAW;CAE3F,QAAQ,CAGR;CAIA,MAAM,UAAU,WAAW;CAC3B,IAAI,SAAS,OAAO,qBAAqB,QAAQ,QAAQ,QAAQ,YAAY,WAAW;CACxF,OAAO,qBAAqB,eAAe,mBAAmB,WAAW;AAC3E"}
@@ -1,67 +0,0 @@
1
- ---
2
- title: 'You Might Not Need A loading.tsx'
3
- description: 'When timber commits the HTTP status code and why it matters.'
4
- slug: 'loading-states'
5
- notAI: true
6
- ---
7
-
8
- # You Might Not Need A `loading.tsx`
9
-
10
- Most React frameworks push you towards building page-level skeletons or loading states. Though it's entirely trivial to build out that experience with timber, we nudge you away from that for several reasons.
11
-
12
- This is not referring to loading states on things like mutations (like submitting a form) or other actions. This is referring to initial (server) or secondary (client) page loading.
13
-
14
- This is how we as frontend engineers make browsing the web a less anxiety-inducing experience.
15
-
16
- ### Loading States Are an Extra Class of UI to Maintain
17
-
18
- You have to design loading states. Decide between spinners and skeletons, and then mimic your UI in the skeleton.
19
-
20
- ### Loading States Cause Content Layout Shift
21
-
22
- Loading states are fixed and content is most often dynamic both in size (think text length) and count (think number of rows). So even if you work incredibly hard to avoid major CLS, you'll invariably end up with CLS somewhere.
23
-
24
- ### Loading States Are a Flash of UI
25
-
26
- Loading states are inherently temporary, so this adds flashes of content. This causes anxiety as the page spasms before finally reaching an undetermined final state. The user is left in a daze wondering if the page is finally ready.
27
-
28
- Every possible "finite state" of UI is another that you and the user have to keep in context. You'll be _surprised_ how much simpler a website feels when a page has one major representation, vs several.
29
-
30
- ### Loading States Hide Prior Context
31
-
32
- Go to an old PHP site and click a URL. The _browser_ shows a loading state, but the prior page remains visible. The new page only becomes visible once it is ready. There's little advantage to hiding the past context while the user waits for the new context to load.
33
-
34
- ### timber Includes a Global Route-Based Loading State
35
-
36
- On every route change, timber triggers a global page-level toploader loading state. If you want to add a secondary loading state that dims the prior content, you can easily do so.
37
-
38
- ### Loading States Break JavaScript Disabled Requests
39
-
40
- `curl` a page with a `loading.tsx` and you'll get back a shell, not the content. Though people downsell the cost, it genuinely does hinder both SEO and agentic requests.
41
-
42
- Leaning on the web as intended is always better than assuming _all_ consumers have JavaScript enabled. Plus, sticking JavaScript in between your _hot_ rendering path is _inherently_ slower. It's just not necessary when you adopt a comprehensive web architecture like React Server Components.
43
-
44
- ### Loading States Break Status Codes
45
-
46
- Loading states flush before the content is even fetched. This feels _faster_, but you are flushing before you know if the user is authenticated, the data exists, or any other class of issue. By flushing immediately, you are forced to send a 200 and lean on JavaScript-injected meta tags to signify errors.
47
-
48
- This breaks the contract of the web for any consumer that isn't a human. Use real status codes, you will gain downstream benefits from doing so.
49
-
50
- ### Most Loading States Paper Over a Bad Data Architecture
51
-
52
- People often use loading states because their database is poorly optimized, or 50ms away from their rendering server, or otherwise. With a well designed data infrastructure, you can often fetch your data plenty fast to render your entire page in < 100ms.
53
-
54
- And when you can't, you can stream and flush those rare pages trivially.
55
-
56
- ### Loading States Still Have Value, but More-So as Opt-In Secondary or Tertiary Content
57
-
58
- To add a loading state as a small sub-section of a page is trivial – just wrap your component in `<Suspense>` and you can defer slower, lower priority content.
59
-
60
- If you decide you want a full page level loading state, you can wrap your layout or page in `<Suspense>` on a case by case basis. No `loading.tsx` needed.
61
-
62
-
63
- --------
64
-
65
- A `loading.tsx` forces your flush point up ever so slightly, but you give up all the things above. Instead, timber asks you to place `<Suspense>` boundaries yourself – thereby _explicitly_ opting into the flush point.
66
-
67
- Now you get a calm, performant website that doesn't have immense content-layout-shift, doesn't hide prior content during loading, and can return proper status codes. The `loading.tsx` seems innocent enough, but its cost both cognitively and architecturally is high.
@@ -1,115 +0,0 @@
1
- ---
2
- title: 'The Flush Point'
3
- description: 'When timber commits the HTTP status code and why it matters.'
4
- slug: 'the-flush-point'
5
- # notAI: true
6
- ---
7
-
8
- # The Flush Point
9
-
10
- The flush point is when the framework sends the first byte of HTML to the browser and commits the HTTP status code. In a traditional PHP website, this happens all at once.
11
-
12
- But modern web frameworks, and notably react server components, have embraced a streaming architecture. Allowing you to send content _after_ the flush point.
13
-
14
- While this is ultimately incredibly powerful, it brings forth confusion because contextually, code can have different side effects depending on _when_ you call it.
15
-
16
- On a next.js app, if I call `notFound()` inside of a `page.tsx` that has a sibling `loading.tsx` – my status code will return `200`. By obfuscating the flush point, we've actually just made developer clarity murky.
17
-
18
- So timber works differently. At no point does timber flush early, unless _you_ (the developer), choose to place a `<Suspense>` boundary. Often this will end up being below your `page.tsx` level, so you'll still have access to controlling and sending proper status codes.
19
-
20
- ------
21
-
22
- AI below..
23
-
24
- ## The Problem
25
-
26
- Most streaming frameworks send a `200 OK` immediately and figure out the real outcome later:
27
-
28
- ```
29
- Request → 200 OK → ... render ... → oh, it's a 404
30
- ```
31
-
32
- By the time the server discovers the page doesn't exist, the status code is already sent. The client sees `200`. Search engines see `200`. CDNs cache it as `200`.
33
-
34
- This breaks search engines (deleted pages never deindex), CDNs (404s get cached as successes), monitoring (zero errors while users see broken pages), and `curl`/scripts (`curl -f` won't detect the failure).
35
-
36
- ## timber's Solution
37
-
38
- timber holds the response until it knows the real outcome:
39
-
40
- ```
41
- Request arrives
42
- → Route matched
43
- → proxy.ts runs
44
- → middleware.ts runs
45
- → access.ts runs
46
- → React shell renders (onShellReady)
47
- → ✓ Status code committed ← flush point
48
- → Shell HTML sent to browser
49
- → Suspense boundaries stream in
50
- ```
51
-
52
- The status code commits when **all three** conditions are met:
53
-
54
- 1. Middleware completed without returning a response
55
- 2. All access checks passed (or denied with a real status code)
56
- 3. React's `onShellReady` fired — the synchronous shell rendered without error
57
-
58
- A missing page returns a real `404`. A failed auth check returns a real `403`. A redirect returns a real `302`. The HTTP layer tells the truth.
59
-
60
- ## Before and After the Flush
61
-
62
- **Before the flush** (blocking):
63
-
64
- - `proxy.ts` — global request processing
65
- - `middleware.ts` — route-level request processing
66
- - `access.ts` — authorization gates
67
- - Synchronous component rendering (the shell)
68
-
69
- **After the flush** (streaming):
70
-
71
- - `<Suspense>` boundaries resolve and stream in
72
- - Slow data loads complete
73
- - The page progressively fills in
74
-
75
- ``` leading="none"
76
- ┌─────── flush point
77
- │
78
- ▼
79
- ├────────┤──────────────────────────┤
80
- blocking streaming
81
- (shell) (suspense boundaries)
82
- ```
83
-
84
- Everything before the flush determines the status code. Everything after streams progressively. You control the boundary by choosing what goes inside `<Suspense>` and what doesn't.
85
-
86
- ## Pages Work Without JavaScript
87
-
88
- Because timber holds the flush until the shell is complete, the browser receives a fully-formed HTML document. Content is visible, links work as standard `<a>` tags, and forms submit as standard POSTs. JavaScript adds client-side navigation, interactive components, and streaming updates — but the page works without it.
89
-
90
- This isn't a special mode. It's how timber works by default.
91
-
92
- ## Early Hints
93
-
94
- timber doesn't make you wait for assets while the shell renders. At route-match time — before middleware runs — timber sends `103 Early Hints` with CSS, JS, and font URLs. The browser starts downloading assets while the server is still working:
95
-
96
- ```
97
- 103 Early Hints
98
- Link: </styles/main.css>; rel=preload; as=style
99
- Link: </chunks/page-abc.js>; rel=modulepreload
100
-
101
- ... middleware runs, shell renders ...
102
-
103
- 200 OK
104
- <html>...
105
- ```
106
-
107
- You get the correctness of a held flush with the performance of early resource loading.
108
-
109
- ## How to Think About It
110
-
111
- Primary content — the data that defines whether a page exists, who can see it, and what it contains — should load before the flush. Put it in your components directly.
112
-
113
- Secondary content — recommendations, activity feeds, analytics widgets — can load after the flush. Wrap it in `<Suspense>`.
114
-
115
- The flush point is the dividing line between "what the page _is_" and "what the page _also shows_."
@@ -1,176 +0,0 @@
1
- ---
2
- title: 'Client Navigation'
3
- description: 'The Link component, useRouter, segment cache, and scroll restoration.'
4
- slug: 'client-navigation'
5
- ---
6
-
7
- # Client Navigation
8
-
9
- timber.js supports client-side navigation via the `<Link>` component. Clicking a link fetches an RSC payload from the server and reconciles the DOM without a full page reload.
10
-
11
- ## `<Link>`
12
-
13
- ```tsx
14
- import { Link } from '@timber-js/app/client';
15
-
16
- <Link href="/about">About</Link>
17
- <Link href="/products/123">Product</Link>
18
- <Link href="/dashboard" replace>Dashboard</Link>
19
- ```
20
-
21
- `<Link>` type-checks `href` against the generated route map. Invalid routes produce a TypeScript error.
22
-
23
- ## `useRouter`
24
-
25
- For programmatic navigation:
26
-
27
- ```tsx title="app/components/logout-button.tsx"
28
- 'use client';
29
-
30
- import { useRouter } from '@timber-js/app/client';
31
-
32
- declare function logout(): Promise<void>;
33
-
34
- export function LogoutButton() {
35
- const router = useRouter();
36
-
37
- async function handleLogout() {
38
- await logout();
39
- router.push('/login');
40
- }
41
-
42
- return <button onClick={handleLogout}>Log out</button>;
43
- }
44
- ```
45
-
46
- | Method | Description |
47
- | ---------------------- | ---------------------------------------- |
48
- | `router.push(href)` | Navigate to a new URL |
49
- | `router.replace(href)` | Navigate without adding a history entry |
50
- | `router.refresh()` | Re-fetch the current route's RSC payload |
51
- | `router.back()` | Go back in history |
52
- | `router.forward()` | Go forward in history |
53
-
54
- ## `usePathname`
55
-
56
- Returns the current pathname:
57
-
58
- ```tsx
59
- 'use client';
60
- import { usePathname, Link } from '@timber-js/app/client';
61
-
62
- export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
63
- const pathname = usePathname();
64
- const isActive = pathname === href;
65
- return (
66
- <Link href={href} className={isActive ? 'font-bold' : ''}>
67
- {children}
68
- </Link>
69
- );
70
- }
71
- ```
72
-
73
- ## `useSelectedLayoutSegment`
74
-
75
- Returns the active segment within a layout — useful for highlighting nav items:
76
-
77
- ```tsx
78
- 'use client';
79
- import { useSelectedLayoutSegment } from '@timber-js/app/client';
80
-
81
- export function DashboardNav() {
82
- const segment = useSelectedLayoutSegment();
83
- // segment is "settings", "projects", etc.
84
- }
85
- ```
86
-
87
- ## `useLinkStatus`
88
-
89
- Track per-link pending state during navigation:
90
-
91
- ```tsx
92
- 'use client';
93
- import { Link, useLinkStatus } from '@timber-js/app/client';
94
-
95
- function NavItemInner({ children }: { children: React.ReactNode }) {
96
- const { isPending } = useLinkStatus();
97
- return <span className={isPending ? 'opacity-50' : ''}>{children}</span>;
98
- }
99
-
100
- export function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
101
- return (
102
- <Link href={href}>
103
- <NavItemInner>{children}</NavItemInner>
104
- </Link>
105
- );
106
- }
107
- ```
108
-
109
- ## Segment Cache
110
-
111
- Opt-in via `clientSegmentCache: true` in `timber.config.ts`. When enabled, the client maintains a mirror of the server's segment tree. On navigation, only changed segments are re-fetched:
112
-
113
- ```ts title="timber.config.ts"
114
- export default {
115
- clientSegmentCache: true,
116
- };
117
- ```
118
-
119
- ```
120
- /dashboard/settings → /dashboard/team
121
-
122
- Root Layout ← sync, mounted → skip
123
- Auth Layout ← sync, mounted → skip
124
- Dashboard Layout ← sync, mounted → skip
125
- Team Page ← new → fetch from server
126
- ```
127
-
128
- Sync layouts stay cached while mounted. Async layouts always re-render. Pages always re-render. Client component state in shared layouts is preserved — counters, form inputs, scroll positions survive navigation.
129
-
130
- When disabled (the default), every client navigation gets a full RSC payload from the server. This is simpler and avoids edge cases with stale cached layouts, at the cost of slightly larger payloads on navigation.
131
-
132
- Back/forward navigation replays cached RSC payloads instantly — no server roundtrip.
133
-
134
- ## Scroll Restoration
135
-
136
- - **Forward navigation** scrolls to the top of the page.
137
- - **Back/forward** restores the saved scroll position.
138
- - **Hash fragments** — `<Link href="/docs/api#install">` commits the full URL (including the `#fragment`) to the address bar and scrolls to the matching element after render, just like a plain `<a>` without JavaScript. If no element matches, navigation falls back to scroll-to-top.
139
-
140
- Pass `scroll={false}` to `<Link>` or `router.push` to preserve the current scroll position:
141
-
142
- ```tsx
143
- <Link href="/dashboard/settings" scroll={false}>Settings</Link>
144
- ```
145
-
146
- ```tsx
147
- router.push('/dashboard/settings', { scroll: false });
148
- ```
149
-
150
- ### Custom Scroll Containers
151
-
152
- If your layout uses a custom scrollable container, add `data-timber-scroll-restoration`:
153
-
154
- ```tsx
155
- <main className="overflow-y-auto h-screen" data-timber-scroll-restoration>
156
- {children}
157
- </main>
158
- ```
159
-
160
- ## `Link.onNavigate`
161
-
162
- Intercept navigation for view transitions:
163
-
164
- ```tsx
165
- <Link
166
- href="/gallery"
167
- onNavigate={(e) => {
168
- if (document.startViewTransition) {
169
- e.preventDefault();
170
- document.startViewTransition(() => e.navigate());
171
- }
172
- }}
173
- >
174
- Gallery
175
- </Link>
176
- ```