@timber-js/app 0.1.0

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 (310) hide show
  1. package/bin/timber.mjs +5 -0
  2. package/dist/_chunks/error-boundary-dj-WO5uq.js +121 -0
  3. package/dist/_chunks/error-boundary-dj-WO5uq.js.map +1 -0
  4. package/dist/_chunks/format-DNt20Kt8.js +163 -0
  5. package/dist/_chunks/format-DNt20Kt8.js.map +1 -0
  6. package/dist/_chunks/interception-DIaZN1bF.js +669 -0
  7. package/dist/_chunks/interception-DIaZN1bF.js.map +1 -0
  8. package/dist/_chunks/metadata-routes-BDnswgRO.js +141 -0
  9. package/dist/_chunks/metadata-routes-BDnswgRO.js.map +1 -0
  10. package/dist/_chunks/registry-DUIpYD_x.js +20 -0
  11. package/dist/_chunks/registry-DUIpYD_x.js.map +1 -0
  12. package/dist/_chunks/request-context-D6XHINkR.js +330 -0
  13. package/dist/_chunks/request-context-D6XHINkR.js.map +1 -0
  14. package/dist/_chunks/tracing-BtOwb8O6.js +174 -0
  15. package/dist/_chunks/tracing-BtOwb8O6.js.map +1 -0
  16. package/dist/_chunks/use-cookie-8ZlA0rr3.js +125 -0
  17. package/dist/_chunks/use-cookie-8ZlA0rr3.js.map +1 -0
  18. package/dist/adapters/cloudflare.d.ts +92 -0
  19. package/dist/adapters/cloudflare.d.ts.map +1 -0
  20. package/dist/adapters/cloudflare.js +188 -0
  21. package/dist/adapters/cloudflare.js.map +1 -0
  22. package/dist/adapters/nitro.d.ts +72 -0
  23. package/dist/adapters/nitro.d.ts.map +1 -0
  24. package/dist/adapters/nitro.js +217 -0
  25. package/dist/adapters/nitro.js.map +1 -0
  26. package/dist/adapters/types.d.ts +53 -0
  27. package/dist/adapters/types.d.ts.map +1 -0
  28. package/dist/cache/index.d.ts +52 -0
  29. package/dist/cache/index.d.ts.map +1 -0
  30. package/dist/cache/index.js +283 -0
  31. package/dist/cache/index.js.map +1 -0
  32. package/dist/cache/redis-handler.d.ts +45 -0
  33. package/dist/cache/redis-handler.d.ts.map +1 -0
  34. package/dist/cache/register-cached-function.d.ts +17 -0
  35. package/dist/cache/register-cached-function.d.ts.map +1 -0
  36. package/dist/cache/singleflight.d.ts +11 -0
  37. package/dist/cache/singleflight.d.ts.map +1 -0
  38. package/dist/cache/stable-stringify.d.ts +7 -0
  39. package/dist/cache/stable-stringify.d.ts.map +1 -0
  40. package/dist/cache/timber-cache.d.ts +21 -0
  41. package/dist/cache/timber-cache.d.ts.map +1 -0
  42. package/dist/cli.d.ts +44 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +135 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/client/browser-entry.d.ts +22 -0
  47. package/dist/client/browser-entry.d.ts.map +1 -0
  48. package/dist/client/error-boundary.d.ts +42 -0
  49. package/dist/client/error-boundary.d.ts.map +1 -0
  50. package/dist/client/form.d.ts +115 -0
  51. package/dist/client/form.d.ts.map +1 -0
  52. package/dist/client/head.d.ts +16 -0
  53. package/dist/client/head.d.ts.map +1 -0
  54. package/dist/client/history.d.ts +29 -0
  55. package/dist/client/history.d.ts.map +1 -0
  56. package/dist/client/index.d.ts +32 -0
  57. package/dist/client/index.d.ts.map +1 -0
  58. package/dist/client/index.js +1218 -0
  59. package/dist/client/index.js.map +1 -0
  60. package/dist/client/link-navigate-interceptor.d.ts +28 -0
  61. package/dist/client/link-navigate-interceptor.d.ts.map +1 -0
  62. package/dist/client/link-status-provider.d.ts +11 -0
  63. package/dist/client/link-status-provider.d.ts.map +1 -0
  64. package/dist/client/link.d.ts +119 -0
  65. package/dist/client/link.d.ts.map +1 -0
  66. package/dist/client/nuqs-adapter.d.ts +11 -0
  67. package/dist/client/nuqs-adapter.d.ts.map +1 -0
  68. package/dist/client/router-ref.d.ts +11 -0
  69. package/dist/client/router-ref.d.ts.map +1 -0
  70. package/dist/client/router.d.ts +85 -0
  71. package/dist/client/router.d.ts.map +1 -0
  72. package/dist/client/segment-cache.d.ts +88 -0
  73. package/dist/client/segment-cache.d.ts.map +1 -0
  74. package/dist/client/segment-context.d.ts +32 -0
  75. package/dist/client/segment-context.d.ts.map +1 -0
  76. package/dist/client/ssr-data.d.ts +64 -0
  77. package/dist/client/ssr-data.d.ts.map +1 -0
  78. package/dist/client/types.d.ts +5 -0
  79. package/dist/client/types.d.ts.map +1 -0
  80. package/dist/client/unload-guard.d.ts +18 -0
  81. package/dist/client/unload-guard.d.ts.map +1 -0
  82. package/dist/client/use-cookie.d.ts +37 -0
  83. package/dist/client/use-cookie.d.ts.map +1 -0
  84. package/dist/client/use-link-status.d.ts +35 -0
  85. package/dist/client/use-link-status.d.ts.map +1 -0
  86. package/dist/client/use-navigation-pending.d.ts +26 -0
  87. package/dist/client/use-navigation-pending.d.ts.map +1 -0
  88. package/dist/client/use-params.d.ts +50 -0
  89. package/dist/client/use-params.d.ts.map +1 -0
  90. package/dist/client/use-pathname.d.ts +20 -0
  91. package/dist/client/use-pathname.d.ts.map +1 -0
  92. package/dist/client/use-query-states.d.ts +36 -0
  93. package/dist/client/use-query-states.d.ts.map +1 -0
  94. package/dist/client/use-router.d.ts +39 -0
  95. package/dist/client/use-router.d.ts.map +1 -0
  96. package/dist/client/use-search-params.d.ts +24 -0
  97. package/dist/client/use-search-params.d.ts.map +1 -0
  98. package/dist/client/use-selected-layout-segment.d.ts +68 -0
  99. package/dist/client/use-selected-layout-segment.d.ts.map +1 -0
  100. package/dist/content/index.d.ts +11 -0
  101. package/dist/content/index.d.ts.map +1 -0
  102. package/dist/content/index.js +2 -0
  103. package/dist/cookies/define-cookie.d.ts +61 -0
  104. package/dist/cookies/define-cookie.d.ts.map +1 -0
  105. package/dist/cookies/index.d.ts +3 -0
  106. package/dist/cookies/index.d.ts.map +1 -0
  107. package/dist/cookies/index.js +82 -0
  108. package/dist/cookies/index.js.map +1 -0
  109. package/dist/fonts/ast.d.ts +38 -0
  110. package/dist/fonts/ast.d.ts.map +1 -0
  111. package/dist/fonts/css.d.ts +43 -0
  112. package/dist/fonts/css.d.ts.map +1 -0
  113. package/dist/fonts/fallbacks.d.ts +36 -0
  114. package/dist/fonts/fallbacks.d.ts.map +1 -0
  115. package/dist/fonts/google.d.ts +122 -0
  116. package/dist/fonts/google.d.ts.map +1 -0
  117. package/dist/fonts/local.d.ts +76 -0
  118. package/dist/fonts/local.d.ts.map +1 -0
  119. package/dist/fonts/types.d.ts +85 -0
  120. package/dist/fonts/types.d.ts.map +1 -0
  121. package/dist/index.d.ts +150 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +14701 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/plugins/adapter-build.d.ts +18 -0
  126. package/dist/plugins/adapter-build.d.ts.map +1 -0
  127. package/dist/plugins/build-manifest.d.ts +79 -0
  128. package/dist/plugins/build-manifest.d.ts.map +1 -0
  129. package/dist/plugins/build-report.d.ts +63 -0
  130. package/dist/plugins/build-report.d.ts.map +1 -0
  131. package/dist/plugins/cache-transform.d.ts +36 -0
  132. package/dist/plugins/cache-transform.d.ts.map +1 -0
  133. package/dist/plugins/chunks.d.ts +45 -0
  134. package/dist/plugins/chunks.d.ts.map +1 -0
  135. package/dist/plugins/content.d.ts +19 -0
  136. package/dist/plugins/content.d.ts.map +1 -0
  137. package/dist/plugins/dev-error-overlay.d.ts +60 -0
  138. package/dist/plugins/dev-error-overlay.d.ts.map +1 -0
  139. package/dist/plugins/dev-logs.d.ts +46 -0
  140. package/dist/plugins/dev-logs.d.ts.map +1 -0
  141. package/dist/plugins/dev-server.d.ts +22 -0
  142. package/dist/plugins/dev-server.d.ts.map +1 -0
  143. package/dist/plugins/dynamic-transform.d.ts +72 -0
  144. package/dist/plugins/dynamic-transform.d.ts.map +1 -0
  145. package/dist/plugins/entries.d.ts +21 -0
  146. package/dist/plugins/entries.d.ts.map +1 -0
  147. package/dist/plugins/fonts.d.ts +77 -0
  148. package/dist/plugins/fonts.d.ts.map +1 -0
  149. package/dist/plugins/mdx.d.ts +21 -0
  150. package/dist/plugins/mdx.d.ts.map +1 -0
  151. package/dist/plugins/react-prod.d.ts +18 -0
  152. package/dist/plugins/react-prod.d.ts.map +1 -0
  153. package/dist/plugins/routing.d.ts +13 -0
  154. package/dist/plugins/routing.d.ts.map +1 -0
  155. package/dist/plugins/server-action-exports.d.ts +26 -0
  156. package/dist/plugins/server-action-exports.d.ts.map +1 -0
  157. package/dist/plugins/server-bundle.d.ts +15 -0
  158. package/dist/plugins/server-bundle.d.ts.map +1 -0
  159. package/dist/plugins/shims.d.ts +18 -0
  160. package/dist/plugins/shims.d.ts.map +1 -0
  161. package/dist/plugins/static-build.d.ts +55 -0
  162. package/dist/plugins/static-build.d.ts.map +1 -0
  163. package/dist/routing/codegen.d.ts +29 -0
  164. package/dist/routing/codegen.d.ts.map +1 -0
  165. package/dist/routing/index.d.ts +8 -0
  166. package/dist/routing/index.d.ts.map +1 -0
  167. package/dist/routing/index.js +2 -0
  168. package/dist/routing/interception.d.ts +46 -0
  169. package/dist/routing/interception.d.ts.map +1 -0
  170. package/dist/routing/scanner.d.ts +28 -0
  171. package/dist/routing/scanner.d.ts.map +1 -0
  172. package/dist/routing/status-file-lint.d.ts +33 -0
  173. package/dist/routing/status-file-lint.d.ts.map +1 -0
  174. package/dist/routing/types.d.ts +81 -0
  175. package/dist/routing/types.d.ts.map +1 -0
  176. package/dist/search-params/analyze.d.ts +54 -0
  177. package/dist/search-params/analyze.d.ts.map +1 -0
  178. package/dist/search-params/codecs.d.ts +53 -0
  179. package/dist/search-params/codecs.d.ts.map +1 -0
  180. package/dist/search-params/create.d.ts +106 -0
  181. package/dist/search-params/create.d.ts.map +1 -0
  182. package/dist/search-params/index.d.ts +7 -0
  183. package/dist/search-params/index.d.ts.map +1 -0
  184. package/dist/search-params/index.js +300 -0
  185. package/dist/search-params/index.js.map +1 -0
  186. package/dist/search-params/registry.d.ts +20 -0
  187. package/dist/search-params/registry.d.ts.map +1 -0
  188. package/dist/server/access-gate.d.ts +42 -0
  189. package/dist/server/access-gate.d.ts.map +1 -0
  190. package/dist/server/action-client.d.ts +190 -0
  191. package/dist/server/action-client.d.ts.map +1 -0
  192. package/dist/server/action-handler.d.ts +48 -0
  193. package/dist/server/action-handler.d.ts.map +1 -0
  194. package/dist/server/actions.d.ts +108 -0
  195. package/dist/server/actions.d.ts.map +1 -0
  196. package/dist/server/asset-headers.d.ts +42 -0
  197. package/dist/server/asset-headers.d.ts.map +1 -0
  198. package/dist/server/body-limits.d.ts +30 -0
  199. package/dist/server/body-limits.d.ts.map +1 -0
  200. package/dist/server/build-manifest.d.ts +120 -0
  201. package/dist/server/build-manifest.d.ts.map +1 -0
  202. package/dist/server/canonicalize.d.ts +30 -0
  203. package/dist/server/canonicalize.d.ts.map +1 -0
  204. package/dist/server/client-module-map.d.ts +47 -0
  205. package/dist/server/client-module-map.d.ts.map +1 -0
  206. package/dist/server/csrf.d.ts +34 -0
  207. package/dist/server/csrf.d.ts.map +1 -0
  208. package/dist/server/deny-renderer.d.ts +49 -0
  209. package/dist/server/deny-renderer.d.ts.map +1 -0
  210. package/dist/server/dev-logger.d.ts +44 -0
  211. package/dist/server/dev-logger.d.ts.map +1 -0
  212. package/dist/server/dev-span-processor.d.ts +29 -0
  213. package/dist/server/dev-span-processor.d.ts.map +1 -0
  214. package/dist/server/dev-warnings.d.ts +129 -0
  215. package/dist/server/dev-warnings.d.ts.map +1 -0
  216. package/dist/server/early-hints-sender.d.ts +38 -0
  217. package/dist/server/early-hints-sender.d.ts.map +1 -0
  218. package/dist/server/early-hints.d.ts +83 -0
  219. package/dist/server/early-hints.d.ts.map +1 -0
  220. package/dist/server/error-boundary-wrapper.d.ts +17 -0
  221. package/dist/server/error-boundary-wrapper.d.ts.map +1 -0
  222. package/dist/server/error-formatter.d.ts +17 -0
  223. package/dist/server/error-formatter.d.ts.map +1 -0
  224. package/dist/server/flush.d.ts +74 -0
  225. package/dist/server/flush.d.ts.map +1 -0
  226. package/dist/server/form-data.d.ts +60 -0
  227. package/dist/server/form-data.d.ts.map +1 -0
  228. package/dist/server/form-flash.d.ts +78 -0
  229. package/dist/server/form-flash.d.ts.map +1 -0
  230. package/dist/server/html-injectors.d.ts +101 -0
  231. package/dist/server/html-injectors.d.ts.map +1 -0
  232. package/dist/server/index.d.ts +54 -0
  233. package/dist/server/index.d.ts.map +1 -0
  234. package/dist/server/index.js +2925 -0
  235. package/dist/server/index.js.map +1 -0
  236. package/dist/server/instrumentation.d.ts +61 -0
  237. package/dist/server/instrumentation.d.ts.map +1 -0
  238. package/dist/server/logger.d.ts +83 -0
  239. package/dist/server/logger.d.ts.map +1 -0
  240. package/dist/server/manifest-status-resolver.d.ts +58 -0
  241. package/dist/server/manifest-status-resolver.d.ts.map +1 -0
  242. package/dist/server/metadata-render.d.ts +20 -0
  243. package/dist/server/metadata-render.d.ts.map +1 -0
  244. package/dist/server/metadata-routes.d.ts +67 -0
  245. package/dist/server/metadata-routes.d.ts.map +1 -0
  246. package/dist/server/metadata.d.ts +67 -0
  247. package/dist/server/metadata.d.ts.map +1 -0
  248. package/dist/server/middleware-runner.d.ts +21 -0
  249. package/dist/server/middleware-runner.d.ts.map +1 -0
  250. package/dist/server/nuqs-ssr-provider.d.ts +28 -0
  251. package/dist/server/nuqs-ssr-provider.d.ts.map +1 -0
  252. package/dist/server/pipeline.d.ts +81 -0
  253. package/dist/server/pipeline.d.ts.map +1 -0
  254. package/dist/server/prerender.d.ts +77 -0
  255. package/dist/server/prerender.d.ts.map +1 -0
  256. package/dist/server/primitives.d.ts +131 -0
  257. package/dist/server/primitives.d.ts.map +1 -0
  258. package/dist/server/proxy.d.ts +23 -0
  259. package/dist/server/proxy.d.ts.map +1 -0
  260. package/dist/server/request-context.d.ts +175 -0
  261. package/dist/server/request-context.d.ts.map +1 -0
  262. package/dist/server/route-element-builder.d.ts +66 -0
  263. package/dist/server/route-element-builder.d.ts.map +1 -0
  264. package/dist/server/route-handler.d.ts +35 -0
  265. package/dist/server/route-handler.d.ts.map +1 -0
  266. package/dist/server/route-matcher.d.ts +78 -0
  267. package/dist/server/route-matcher.d.ts.map +1 -0
  268. package/dist/server/rsc-entry/api-handler.d.ts +11 -0
  269. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -0
  270. package/dist/server/rsc-entry/error-renderer.d.ts +30 -0
  271. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -0
  272. package/dist/server/rsc-entry/helpers.d.ts +73 -0
  273. package/dist/server/rsc-entry/helpers.d.ts.map +1 -0
  274. package/dist/server/rsc-entry/index.d.ts +11 -0
  275. package/dist/server/rsc-entry/index.d.ts.map +1 -0
  276. package/dist/server/rsc-entry/ssr-bridge.d.ts +6 -0
  277. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -0
  278. package/dist/server/slot-resolver.d.ts +34 -0
  279. package/dist/server/slot-resolver.d.ts.map +1 -0
  280. package/dist/server/ssr-entry.d.ts +73 -0
  281. package/dist/server/ssr-entry.d.ts.map +1 -0
  282. package/dist/server/ssr-render.d.ts +67 -0
  283. package/dist/server/ssr-render.d.ts.map +1 -0
  284. package/dist/server/status-code-resolver.d.ts +77 -0
  285. package/dist/server/status-code-resolver.d.ts.map +1 -0
  286. package/dist/server/tracing.d.ts +99 -0
  287. package/dist/server/tracing.d.ts.map +1 -0
  288. package/dist/server/tree-builder.d.ts +116 -0
  289. package/dist/server/tree-builder.d.ts.map +1 -0
  290. package/dist/server/types.d.ts +231 -0
  291. package/dist/server/types.d.ts.map +1 -0
  292. package/dist/shims/font-google.d.ts +41 -0
  293. package/dist/shims/font-google.d.ts.map +1 -0
  294. package/dist/shims/headers.d.ts +11 -0
  295. package/dist/shims/headers.d.ts.map +1 -0
  296. package/dist/shims/image.d.ts +328 -0
  297. package/dist/shims/image.d.ts.map +1 -0
  298. package/dist/shims/link.d.ts +9 -0
  299. package/dist/shims/link.d.ts.map +1 -0
  300. package/dist/shims/navigation-client.d.ts +25 -0
  301. package/dist/shims/navigation-client.d.ts.map +1 -0
  302. package/dist/shims/navigation.d.ts +25 -0
  303. package/dist/shims/navigation.d.ts.map +1 -0
  304. package/dist/utils/directive-parser.d.ts +70 -0
  305. package/dist/utils/directive-parser.d.ts.map +1 -0
  306. package/dist/utils/format.d.ts +6 -0
  307. package/dist/utils/format.d.ts.map +1 -0
  308. package/dist/utils/startup-timer.d.ts +34 -0
  309. package/dist/utils/startup-timer.d.ts.map +1 -0
  310. package/package.json +140 -0
@@ -0,0 +1,1218 @@
1
+ import { t as TimberErrorBoundary } from "../_chunks/error-boundary-dj-WO5uq.js";
2
+ import { i as setSsrData, n as clearSsrData, r as getSsrData, t as useCookie } from "../_chunks/use-cookie-8ZlA0rr3.js";
3
+ import { t as getSearchParams$1 } from "../_chunks/registry-DUIpYD_x.js";
4
+ import { createContext, createElement, useActionState as useActionState$1, useContext, useEffect, useMemo, useRef, useSyncExternalStore, useTransition } from "react";
5
+ import { jsxDEV } from "react/jsx-dev-runtime";
6
+ import { useQueryStates as useQueryStates$1 } from "nuqs";
7
+ //#region src/client/link-navigate-interceptor.tsx
8
+ var _jsxFileName$2 = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link-navigate-interceptor.tsx";
9
+ /** Symbol used to store the onNavigate callback on anchor elements. */
10
+ var ON_NAVIGATE_KEY = "__timberOnNavigate";
11
+ /**
12
+ * Client component rendered inside <Link> that attaches the onNavigate
13
+ * callback to the closest <a> ancestor via a DOM property. The callback
14
+ * is cleaned up on unmount.
15
+ *
16
+ * Renders no extra DOM — just a transparent wrapper.
17
+ */
18
+ function LinkNavigateInterceptor({ onNavigate, children }) {
19
+ const ref = useRef(null);
20
+ useEffect(() => {
21
+ const anchor = ref.current?.closest("a");
22
+ if (!anchor) return;
23
+ anchor[ON_NAVIGATE_KEY] = onNavigate;
24
+ return () => {
25
+ delete anchor[ON_NAVIGATE_KEY];
26
+ };
27
+ }, [onNavigate]);
28
+ return /* @__PURE__ */ jsxDEV("span", {
29
+ ref,
30
+ style: { display: "contents" },
31
+ children
32
+ }, void 0, false, {
33
+ fileName: _jsxFileName$2,
34
+ lineNumber: 58,
35
+ columnNumber: 5
36
+ }, this);
37
+ }
38
+ //#endregion
39
+ //#region src/client/use-link-status.ts
40
+ /**
41
+ * React context provided by <Link>. Holds the pending status
42
+ * for that specific link's navigation.
43
+ */
44
+ var LinkStatusContext = createContext({ pending: false });
45
+ /**
46
+ * Returns `{ pending: true }` while the nearest parent `<Link>` component's
47
+ * navigation is in flight. Must be used inside a `<Link>` component's children.
48
+ *
49
+ * Unlike `useNavigationPending()` which is global, this hook is scoped to
50
+ * the nearest parent `<Link>` — only the link the user clicked shows pending.
51
+ *
52
+ * ```tsx
53
+ * 'use client'
54
+ * import { Link, useLinkStatus } from '@timber/app/client'
55
+ *
56
+ * function Hint() {
57
+ * const { pending } = useLinkStatus()
58
+ * return <span className={pending ? 'opacity-50' : ''} />
59
+ * }
60
+ *
61
+ * export function NavLink({ href, children }) {
62
+ * return (
63
+ * <Link href={href}>
64
+ * {children} <Hint />
65
+ * </Link>
66
+ * )
67
+ * }
68
+ * ```
69
+ */
70
+ function useLinkStatus() {
71
+ return useContext(LinkStatusContext);
72
+ }
73
+ //#endregion
74
+ //#region src/client/router-ref.ts
75
+ var globalRouter = null;
76
+ /**
77
+ * Get the global router instance. Throws if called before bootstrap.
78
+ * Used by client-side hooks (useNavigationPending, etc.)
79
+ */
80
+ function getRouter() {
81
+ if (!globalRouter) throw new Error("[timber] Router not initialized. getRouter() was called before bootstrap().");
82
+ return globalRouter;
83
+ }
84
+ //#endregion
85
+ //#region src/client/link-status-provider.tsx
86
+ var _jsxFileName$1 = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link-status-provider.tsx";
87
+ var NOT_PENDING = { pending: false };
88
+ var IS_PENDING = { pending: true };
89
+ /**
90
+ * Client component that subscribes to the router's pending URL and provides
91
+ * a scoped LinkStatusContext to children. Renders no extra DOM — just a
92
+ * context provider around children.
93
+ */
94
+ function LinkStatusProvider({ href, children }) {
95
+ const status = useSyncExternalStore((callback) => {
96
+ try {
97
+ return getRouter().onPendingChange(callback);
98
+ } catch {
99
+ return () => {};
100
+ }
101
+ }, () => {
102
+ try {
103
+ if (getRouter().getPendingUrl() === href) return IS_PENDING;
104
+ return NOT_PENDING;
105
+ } catch {
106
+ return NOT_PENDING;
107
+ }
108
+ }, () => NOT_PENDING);
109
+ return /* @__PURE__ */ jsxDEV(LinkStatusContext.Provider, {
110
+ value: status,
111
+ children
112
+ }, void 0, false, {
113
+ fileName: _jsxFileName$1,
114
+ lineNumber: 39,
115
+ columnNumber: 10
116
+ }, this);
117
+ }
118
+ //#endregion
119
+ //#region src/client/link.tsx
120
+ var _jsxFileName = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link.tsx";
121
+ /**
122
+ * Reject dangerous URL schemes that could execute script.
123
+ * Security: design/13-security.md § Link scheme injection (test #9)
124
+ */
125
+ var DANGEROUS_SCHEMES = /^\s*(javascript|data|vbscript):/i;
126
+ function validateLinkHref(href) {
127
+ if (DANGEROUS_SCHEMES.test(href)) throw new Error(`<Link> received a dangerous href: "${href}". javascript:, data:, and vbscript: URLs are not allowed.`);
128
+ }
129
+ /** Returns true if the href is an internal path (not an external URL) */
130
+ function isInternalHref(href) {
131
+ if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return true;
132
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return false;
133
+ return true;
134
+ }
135
+ /**
136
+ * Interpolate dynamic segments in a route pattern with actual values.
137
+ * e.g. interpolateParams("/products/[id]", { id: "123" }) → "/products/123"
138
+ *
139
+ * Supports:
140
+ * - [param] → single segment
141
+ * - [...param] → catch-all (joined with /)
142
+ * - [[...param]] → optional catch-all (omitted if undefined/empty)
143
+ */
144
+ function interpolateParams(pattern, params) {
145
+ return pattern.replace(/\[\[\.\.\.(\w+)\]\]|\[\.\.\.(\w+)\]|\[(\w+)\]/g, (_match, optionalCatchAll, catchAll, single) => {
146
+ if (optionalCatchAll) {
147
+ const value = params[optionalCatchAll];
148
+ if (value === void 0 || Array.isArray(value) && value.length === 0) return "";
149
+ return (Array.isArray(value) ? value : [value]).map(encodeURIComponent).join("/");
150
+ }
151
+ if (catchAll) {
152
+ const value = params[catchAll];
153
+ if (value === void 0) throw new Error(`<Link> missing required catch-all param "${catchAll}" for pattern "${pattern}".`);
154
+ const segments = Array.isArray(value) ? value : [value];
155
+ if (segments.length === 0) throw new Error(`<Link> catch-all param "${catchAll}" must have at least one segment for pattern "${pattern}".`);
156
+ return segments.map(encodeURIComponent).join("/");
157
+ }
158
+ const value = params[single];
159
+ if (value === void 0) throw new Error(`<Link> missing required param "${single}" for pattern "${pattern}".`);
160
+ if (Array.isArray(value)) throw new Error(`<Link> param "${single}" expected a string but received an array for pattern "${pattern}".`);
161
+ return encodeURIComponent(String(value));
162
+ }).replace(/\/+$/, "") || "/";
163
+ }
164
+ /**
165
+ * Resolve the final href string from Link props.
166
+ *
167
+ * Handles:
168
+ * - params interpolation into route patterns
169
+ * - searchParams serialization via SearchParamsDefinition
170
+ * - Validation that searchParams and inline query strings are exclusive
171
+ */
172
+ function resolveHref(href, params, searchParams) {
173
+ let resolvedPath = href;
174
+ if (params) resolvedPath = interpolateParams(href, params);
175
+ if (searchParams) {
176
+ if (resolvedPath.includes("?")) throw new Error("<Link> received both a searchParams prop and a query string in href. These are mutually exclusive — use one or the other.");
177
+ const qs = searchParams.definition.serialize(searchParams.values);
178
+ if (qs) resolvedPath = `${resolvedPath}?${qs}`;
179
+ }
180
+ return resolvedPath;
181
+ }
182
+ /**
183
+ * Build the HTML attributes for a Link. Separated from the component
184
+ * for testability — the component just spreads these onto an <a>.
185
+ */
186
+ function buildLinkProps(props) {
187
+ const resolvedHref = resolveHref(props.href, props.params, props.searchParams);
188
+ validateLinkHref(resolvedHref);
189
+ const output = { href: resolvedHref };
190
+ if (isInternalHref(resolvedHref)) {
191
+ output["data-timber-link"] = true;
192
+ if (props.prefetch) output["data-timber-prefetch"] = true;
193
+ if (props.scroll === false) output["data-timber-scroll"] = "false";
194
+ }
195
+ return output;
196
+ }
197
+ /**
198
+ * Navigation link with progressive enhancement.
199
+ *
200
+ * Renders as a plain `<a>` tag — works without JavaScript. When the client
201
+ * runtime is active, it intercepts clicks on links marked with
202
+ * `data-timber-link` to perform RSC-based client navigation.
203
+ *
204
+ * Supports typed routes via codegen overloads. At runtime:
205
+ * - `params` prop interpolates dynamic segments in the href pattern
206
+ * - `searchParams` prop serializes query parameters via a SearchParamsDefinition
207
+ */
208
+ function Link({ href, prefetch, scroll, params, searchParams, onNavigate, children, ...rest }) {
209
+ const linkProps = buildLinkProps({
210
+ href,
211
+ prefetch,
212
+ scroll,
213
+ params,
214
+ searchParams
215
+ });
216
+ const inner = /* @__PURE__ */ jsxDEV(LinkStatusProvider, {
217
+ href: linkProps.href,
218
+ children
219
+ }, void 0, false, {
220
+ fileName: _jsxFileName,
221
+ lineNumber: 299,
222
+ columnNumber: 17
223
+ }, this);
224
+ return /* @__PURE__ */ jsxDEV("a", {
225
+ ...rest,
226
+ ...linkProps,
227
+ children: onNavigate ? /* @__PURE__ */ jsxDEV(LinkNavigateInterceptor, {
228
+ onNavigate,
229
+ children: inner
230
+ }, void 0, false, {
231
+ fileName: _jsxFileName,
232
+ lineNumber: 304,
233
+ columnNumber: 9
234
+ }, this) : inner
235
+ }, void 0, false, {
236
+ fileName: _jsxFileName,
237
+ lineNumber: 302,
238
+ columnNumber: 5
239
+ }, this);
240
+ }
241
+ //#endregion
242
+ //#region src/client/segment-cache.ts
243
+ /**
244
+ * Maintains the client-side segment tree representing currently mounted
245
+ * layouts and pages. Used for navigation reconciliation — the router diffs
246
+ * new routes against this tree to determine which segments to re-fetch.
247
+ */
248
+ var SegmentCache = class {
249
+ root;
250
+ get(segment) {
251
+ if (segment === "/" || segment === this.root?.segment) return this.root;
252
+ }
253
+ set(segment, node) {
254
+ if (segment === "/" || !this.root) this.root = node;
255
+ }
256
+ clear() {
257
+ this.root = void 0;
258
+ }
259
+ /**
260
+ * Serialize the mounted segment tree for the X-Timber-State-Tree header.
261
+ * Only includes sync segments — async segments are excluded because the
262
+ * server must always re-render them (they may depend on request context).
263
+ *
264
+ * This is a performance optimization only, NOT a security boundary.
265
+ * The server always runs all access.ts files regardless of the state tree.
266
+ */
267
+ serializeStateTree() {
268
+ const segments = [];
269
+ if (this.root) collectSyncSegments(this.root, segments);
270
+ return { segments };
271
+ }
272
+ };
273
+ /** Recursively collect sync segment paths from the tree */
274
+ function collectSyncSegments(node, out) {
275
+ if (!node.isAsync) out.push(node.segment);
276
+ for (const child of node.children.values()) collectSyncSegments(child, out);
277
+ }
278
+ /**
279
+ * Build a SegmentNode tree from flat segment metadata.
280
+ *
281
+ * Takes an ordered list of segment descriptors (root → leaf) from the
282
+ * server's X-Timber-Segments header and constructs the hierarchical
283
+ * tree structure that SegmentCache expects.
284
+ *
285
+ * Each segment is nested as a child of the previous one, forming a
286
+ * linear chain from root to leaf. The leaf segment (page) is excluded
287
+ * from the tree — pages are never cached across navigations.
288
+ */
289
+ function buildSegmentTree(segments) {
290
+ if (segments.length === 0) return void 0;
291
+ const layouts = segments.length > 1 ? segments.slice(0, -1) : segments;
292
+ let root;
293
+ let parent;
294
+ for (const info of layouts) {
295
+ const node = {
296
+ segment: info.path,
297
+ payload: null,
298
+ isAsync: info.isAsync,
299
+ children: /* @__PURE__ */ new Map()
300
+ };
301
+ if (!root) root = node;
302
+ if (parent) parent.children.set(info.path, node);
303
+ parent = node;
304
+ }
305
+ return root;
306
+ }
307
+ /**
308
+ * Short-lived cache for hover-triggered prefetches. Entries expire after
309
+ * 30 seconds. When a link is clicked, the prefetched payload is consumed
310
+ * (moved to the history stack) and removed from this cache.
311
+ *
312
+ * timber.js does NOT prefetch on viewport intersection — only explicit
313
+ * hover on <Link prefetch> triggers a prefetch.
314
+ */
315
+ var PrefetchCache = class PrefetchCache {
316
+ static TTL_MS = 3e4;
317
+ entries = /* @__PURE__ */ new Map();
318
+ set(url, result) {
319
+ this.entries.set(url, {
320
+ result,
321
+ expiresAt: Date.now() + PrefetchCache.TTL_MS
322
+ });
323
+ }
324
+ get(url) {
325
+ const entry = this.entries.get(url);
326
+ if (!entry) return void 0;
327
+ if (Date.now() >= entry.expiresAt) {
328
+ this.entries.delete(url);
329
+ return;
330
+ }
331
+ return entry.result;
332
+ }
333
+ /** Get and remove the entry (used when navigation consumes a prefetch) */
334
+ consume(url) {
335
+ const result = this.get(url);
336
+ if (result !== void 0) this.entries.delete(url);
337
+ return result;
338
+ }
339
+ };
340
+ //#endregion
341
+ //#region src/client/history.ts
342
+ /**
343
+ * Session-lived history stack keyed by URL. Enables instant back/forward
344
+ * navigation without a server roundtrip.
345
+ *
346
+ * On forward navigation, the new page's payload is pushed onto the stack.
347
+ * On popstate, the cached payload is replayed instantly.
348
+ *
349
+ * Scroll positions are stored in history.state (browser History API),
350
+ * not in this stack — see design/19-client-navigation.md §Scroll Restoration.
351
+ *
352
+ * Entries persist for the session duration (no expiry) and are cleared
353
+ * when the tab is closed — matching browser back-button behavior.
354
+ */
355
+ var HistoryStack = class {
356
+ entries = /* @__PURE__ */ new Map();
357
+ push(url, entry) {
358
+ this.entries.set(url, entry);
359
+ }
360
+ get(url) {
361
+ return this.entries.get(url);
362
+ }
363
+ has(url) {
364
+ return this.entries.has(url);
365
+ }
366
+ };
367
+ //#endregion
368
+ //#region src/client/use-params.ts
369
+ var currentParams = {};
370
+ /**
371
+ * Set the current route params. Called by the framework internals
372
+ * during navigation — not intended for direct use by app code.
373
+ *
374
+ * On the client, the segment router calls this on each navigation.
375
+ * During SSR, params are also available via getSsrData().params
376
+ * (ALS-backed), but setCurrentParams is still called for the
377
+ * module-level fallback path.
378
+ */
379
+ function setCurrentParams(params) {
380
+ currentParams = params;
381
+ }
382
+ function useParams(_route) {
383
+ const ssrData = getSsrData();
384
+ if (ssrData) return ssrData.params;
385
+ return currentParams;
386
+ }
387
+ //#endregion
388
+ //#region src/client/router.ts
389
+ /**
390
+ * Thrown when an RSC payload response contains X-Timber-Redirect header.
391
+ * Caught in navigate() to trigger a soft router navigation to the redirect target.
392
+ */
393
+ var RedirectError = class extends Error {
394
+ redirectUrl;
395
+ constructor(url) {
396
+ super(`Server redirect to ${url}`);
397
+ this.redirectUrl = url;
398
+ }
399
+ };
400
+ /**
401
+ * Check if an error is an abort error (connection closed / fetch aborted).
402
+ * Browsers throw DOMException with name 'AbortError' when a fetch is aborted.
403
+ */
404
+ function isAbortError(error) {
405
+ if (error instanceof DOMException && error.name === "AbortError") return true;
406
+ if (error instanceof Error && error.name === "AbortError") return true;
407
+ return false;
408
+ }
409
+ var RSC_CONTENT_TYPE = "text/x-component";
410
+ /**
411
+ * Generate a short random cache-busting ID (5 chars, a-z0-9).
412
+ * Matches the format Next.js uses for _rsc params.
413
+ */
414
+ function generateCacheBustId() {
415
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
416
+ let id = "";
417
+ for (let i = 0; i < 5; i++) id += chars[Math.random() * 36 | 0];
418
+ return id;
419
+ }
420
+ /**
421
+ * Append a `_rsc=<id>` query parameter to the URL.
422
+ * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML
423
+ * for RSC navigation requests and signals that this is an RSC fetch.
424
+ */
425
+ function appendRscParam(url) {
426
+ return `${url}${url.includes("?") ? "&" : "?"}_rsc=${generateCacheBustId()}`;
427
+ }
428
+ function buildRscHeaders(stateTree, currentUrl) {
429
+ const headers = { Accept: RSC_CONTENT_TYPE };
430
+ if (stateTree) headers["X-Timber-State-Tree"] = JSON.stringify(stateTree);
431
+ if (currentUrl) headers["X-Timber-URL"] = currentUrl;
432
+ return headers;
433
+ }
434
+ /**
435
+ * Extract head elements from the X-Timber-Head response header.
436
+ * Returns null if the header is missing or malformed.
437
+ */
438
+ function extractHeadElements(response) {
439
+ const header = response.headers.get("X-Timber-Head");
440
+ if (!header) return null;
441
+ try {
442
+ return JSON.parse(decodeURIComponent(header));
443
+ } catch {
444
+ return null;
445
+ }
446
+ }
447
+ /**
448
+ * Extract segment metadata from the X-Timber-Segments response header.
449
+ * Returns null if the header is missing or malformed.
450
+ *
451
+ * Format: JSON array of {path, isAsync} objects describing the rendered
452
+ * segment chain from root to leaf. Used to populate the client-side
453
+ * segment cache for state tree diffing on subsequent navigations.
454
+ */
455
+ function extractSegmentInfo(response) {
456
+ const header = response.headers.get("X-Timber-Segments");
457
+ if (!header) return null;
458
+ try {
459
+ return JSON.parse(header);
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ /**
465
+ * Extract route params from the X-Timber-Params response header.
466
+ * Returns null if the header is missing or malformed.
467
+ *
468
+ * Used to populate useParams() after client-side navigation.
469
+ */
470
+ function extractParams(response) {
471
+ const header = response.headers.get("X-Timber-Params");
472
+ if (!header) return null;
473
+ try {
474
+ return JSON.parse(header);
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
479
+ /**
480
+ * Fetch an RSC payload from the server. If a decodeRsc function is provided,
481
+ * the response is decoded into a React element tree via createFromFetch.
482
+ * Otherwise, the raw response text is returned (test mode).
483
+ *
484
+ * Also extracts head elements from the X-Timber-Head response header
485
+ * so the client can update document.title and <meta> tags after navigation.
486
+ */
487
+ async function fetchRscPayload(url, deps, stateTree, currentUrl) {
488
+ const rscUrl = appendRscParam(url);
489
+ const headers = buildRscHeaders(stateTree, currentUrl);
490
+ if (deps.decodeRsc) {
491
+ const fetchPromise = deps.fetch(rscUrl, {
492
+ headers,
493
+ redirect: "manual"
494
+ });
495
+ let headElements = null;
496
+ let segmentInfo = null;
497
+ let params = null;
498
+ const wrappedPromise = fetchPromise.then((response) => {
499
+ const redirectLocation = response.headers.get("X-Timber-Redirect") || (response.status >= 300 && response.status < 400 ? response.headers.get("Location") : null);
500
+ if (redirectLocation) throw new RedirectError(redirectLocation);
501
+ headElements = extractHeadElements(response);
502
+ segmentInfo = extractSegmentInfo(response);
503
+ params = extractParams(response);
504
+ return response;
505
+ });
506
+ await wrappedPromise;
507
+ return {
508
+ payload: await deps.decodeRsc(wrappedPromise),
509
+ headElements,
510
+ segmentInfo,
511
+ params
512
+ };
513
+ }
514
+ const response = await deps.fetch(rscUrl, {
515
+ headers,
516
+ redirect: "manual"
517
+ });
518
+ if (response.status >= 300 && response.status < 400) {
519
+ const location = response.headers.get("Location");
520
+ if (location) throw new RedirectError(location);
521
+ }
522
+ return {
523
+ payload: await response.text(),
524
+ headElements: extractHeadElements(response),
525
+ segmentInfo: extractSegmentInfo(response),
526
+ params: extractParams(response)
527
+ };
528
+ }
529
+ /**
530
+ * Create a router instance. In production, called once at app hydration
531
+ * with real browser APIs. In tests, called with mock dependencies.
532
+ */
533
+ function createRouter(deps) {
534
+ const segmentCache = new SegmentCache();
535
+ const prefetchCache = new PrefetchCache();
536
+ const historyStack = new HistoryStack();
537
+ let pending = false;
538
+ let pendingUrl = null;
539
+ const pendingListeners = /* @__PURE__ */ new Set();
540
+ function setPending(value, url) {
541
+ const newPendingUrl = value && url ? url : null;
542
+ if (pending === value && pendingUrl === newPendingUrl) return;
543
+ pending = value;
544
+ pendingUrl = newPendingUrl;
545
+ for (const listener of pendingListeners) listener(value);
546
+ }
547
+ /** Update the segment cache from server-provided segment metadata. */
548
+ function updateSegmentCache(segmentInfo) {
549
+ if (!segmentInfo || segmentInfo.length === 0) return;
550
+ const tree = buildSegmentTree(segmentInfo);
551
+ if (tree) segmentCache.set("/", tree);
552
+ }
553
+ /** Render a decoded RSC payload into the DOM if a renderer is available. */
554
+ function renderPayload(payload) {
555
+ if (deps.renderRoot) deps.renderRoot(payload);
556
+ }
557
+ /** Update useParams() with route params from the server response. */
558
+ function updateParams(params) {
559
+ setCurrentParams(params ?? {});
560
+ }
561
+ /** Apply head elements (title, meta tags) to the DOM if available. */
562
+ function applyHead(elements) {
563
+ if (elements && deps.applyHead) deps.applyHead(elements);
564
+ }
565
+ /** Run a callback after the next paint (after React commit). */
566
+ function afterPaint(callback) {
567
+ if (deps.afterPaint) deps.afterPaint(callback);
568
+ else callback();
569
+ }
570
+ async function navigate(url, options = {}) {
571
+ const scroll = options.scroll !== false;
572
+ const replace = options.replace === true;
573
+ const currentScrollY = deps.getScrollY();
574
+ deps.replaceState({
575
+ timber: true,
576
+ scrollY: currentScrollY
577
+ }, "", deps.getCurrentUrl());
578
+ setPending(true, url);
579
+ try {
580
+ let result = prefetchCache.consume(url);
581
+ if (result === void 0) {
582
+ const stateTree = segmentCache.serializeStateTree();
583
+ const rawCurrentUrl = deps.getCurrentUrl();
584
+ result = await fetchRscPayload(url, deps, stateTree, rawCurrentUrl.startsWith("http") ? new URL(rawCurrentUrl).pathname : new URL(rawCurrentUrl, "http://localhost").pathname);
585
+ }
586
+ if (replace) deps.replaceState({
587
+ timber: true,
588
+ scrollY: 0
589
+ }, "", url);
590
+ else deps.pushState({
591
+ timber: true,
592
+ scrollY: 0
593
+ }, "", url);
594
+ historyStack.push(url, {
595
+ payload: result.payload,
596
+ headElements: result.headElements,
597
+ params: result.params
598
+ });
599
+ updateSegmentCache(result.segmentInfo);
600
+ updateParams(result.params);
601
+ renderPayload(result.payload);
602
+ applyHead(result.headElements);
603
+ window.dispatchEvent(new Event("timber:navigation-end"));
604
+ afterPaint(() => {
605
+ if (scroll) deps.scrollTo(0, 0);
606
+ else deps.scrollTo(0, currentScrollY);
607
+ window.dispatchEvent(new Event("timber:scroll-restored"));
608
+ });
609
+ } catch (error) {
610
+ if (error instanceof RedirectError) {
611
+ setPending(false);
612
+ await navigate(error.redirectUrl, { replace: true });
613
+ return;
614
+ }
615
+ if (isAbortError(error)) return;
616
+ throw error;
617
+ } finally {
618
+ setPending(false);
619
+ }
620
+ }
621
+ async function refresh() {
622
+ const currentUrl = deps.getCurrentUrl();
623
+ setPending(true, currentUrl);
624
+ try {
625
+ const result = await fetchRscPayload(currentUrl, deps);
626
+ historyStack.push(currentUrl, {
627
+ payload: result.payload,
628
+ headElements: result.headElements,
629
+ params: result.params
630
+ });
631
+ updateSegmentCache(result.segmentInfo);
632
+ updateParams(result.params);
633
+ renderPayload(result.payload);
634
+ applyHead(result.headElements);
635
+ } finally {
636
+ setPending(false);
637
+ }
638
+ }
639
+ async function handlePopState(url, scrollY = 0) {
640
+ const entry = historyStack.get(url);
641
+ if (entry && entry.payload !== null) {
642
+ updateParams(entry.params);
643
+ renderPayload(entry.payload);
644
+ applyHead(entry.headElements);
645
+ afterPaint(() => {
646
+ deps.scrollTo(0, scrollY);
647
+ window.dispatchEvent(new Event("timber:scroll-restored"));
648
+ });
649
+ } else {
650
+ setPending(true, url);
651
+ try {
652
+ const result = await fetchRscPayload(url, deps, segmentCache.serializeStateTree());
653
+ updateSegmentCache(result.segmentInfo);
654
+ updateParams(result.params);
655
+ historyStack.push(url, {
656
+ payload: result.payload,
657
+ headElements: result.headElements,
658
+ params: result.params
659
+ });
660
+ renderPayload(result.payload);
661
+ applyHead(result.headElements);
662
+ afterPaint(() => {
663
+ deps.scrollTo(0, scrollY);
664
+ window.dispatchEvent(new Event("timber:scroll-restored"));
665
+ });
666
+ } finally {
667
+ setPending(false);
668
+ }
669
+ }
670
+ }
671
+ /**
672
+ * Prefetch an RSC payload for a URL and store it in the prefetch cache.
673
+ * Called on hover of <Link prefetch> elements.
674
+ */
675
+ function prefetch(url) {
676
+ if (prefetchCache.get(url) !== void 0) return;
677
+ if (historyStack.has(url)) return;
678
+ fetchRscPayload(url, deps, segmentCache.serializeStateTree()).then((result) => {
679
+ prefetchCache.set(url, result);
680
+ }, () => {});
681
+ }
682
+ return {
683
+ navigate,
684
+ refresh,
685
+ handlePopState,
686
+ isPending: () => pending,
687
+ getPendingUrl: () => pendingUrl,
688
+ onPendingChange(listener) {
689
+ pendingListeners.add(listener);
690
+ return () => pendingListeners.delete(listener);
691
+ },
692
+ prefetch,
693
+ applyRevalidation(element, headElements) {
694
+ const currentUrl = deps.getCurrentUrl();
695
+ historyStack.push(currentUrl, {
696
+ payload: element,
697
+ headElements
698
+ });
699
+ renderPayload(element);
700
+ applyHead(headElements);
701
+ },
702
+ initSegmentCache: (segments) => updateSegmentCache(segments),
703
+ segmentCache,
704
+ prefetchCache,
705
+ historyStack
706
+ };
707
+ }
708
+ //#endregion
709
+ //#region src/client/use-navigation-pending.ts
710
+ /**
711
+ * Returns true while an RSC navigation is in flight.
712
+ *
713
+ * The pending state is true from the moment the RSC fetch starts until
714
+ * React reconciliation completes. This includes the fetch itself,
715
+ * RSC stream parsing, and React tree reconciliation.
716
+ *
717
+ * It does NOT include Suspense streaming after the shell — only the
718
+ * initial shell reconciliation.
719
+ *
720
+ * ```tsx
721
+ * 'use client'
722
+ * import { useNavigationPending } from '@timber/app/client'
723
+ *
724
+ * export function NavBar() {
725
+ * const isPending = useNavigationPending()
726
+ * return (
727
+ * <nav className={isPending ? 'opacity-50' : ''}>
728
+ * <Link href="/dashboard">Dashboard</Link>
729
+ * </nav>
730
+ * )
731
+ * }
732
+ * ```
733
+ */
734
+ function useNavigationPending() {
735
+ return useSyncExternalStore((callback) => {
736
+ return getRouter().onPendingChange(callback);
737
+ }, () => {
738
+ try {
739
+ return getRouter().isPending();
740
+ } catch {
741
+ return false;
742
+ }
743
+ }, () => false);
744
+ }
745
+ //#endregion
746
+ //#region src/client/use-router.ts
747
+ /**
748
+ * useRouter() — client-side hook for programmatic navigation.
749
+ *
750
+ * Returns a router instance with push, replace, refresh, back, forward,
751
+ * and prefetch methods. Compatible with Next.js's `useRouter()` from
752
+ * `next/navigation` (App Router).
753
+ *
754
+ * This wraps timber's internal RouterInstance in the Next.js-compatible
755
+ * AppRouterInstance shape that ecosystem libraries expect.
756
+ */
757
+ /** No-op router returned during SSR or before bootstrap. All methods are safe no-ops. */
758
+ var SSR_NOOP_ROUTER = {
759
+ push() {},
760
+ replace() {},
761
+ refresh() {},
762
+ back() {},
763
+ forward() {},
764
+ prefetch() {}
765
+ };
766
+ /**
767
+ * Get a router instance for programmatic navigation.
768
+ *
769
+ * Compatible with Next.js's `useRouter()` from `next/navigation`.
770
+ *
771
+ * Returns a no-op router during SSR or before the client router is bootstrapped,
772
+ * so components that call useRouter() at the function level (e.g. TransitionLink)
773
+ * do not crash during server-side rendering.
774
+ */
775
+ function useRouter() {
776
+ let router;
777
+ try {
778
+ router = getRouter();
779
+ } catch {
780
+ return SSR_NOOP_ROUTER;
781
+ }
782
+ return {
783
+ push(href, options) {
784
+ router.navigate(href, { scroll: options?.scroll });
785
+ },
786
+ replace(href, options) {
787
+ router.navigate(href, {
788
+ scroll: options?.scroll,
789
+ replace: true
790
+ });
791
+ },
792
+ refresh() {
793
+ router.refresh();
794
+ },
795
+ back() {
796
+ window.history.back();
797
+ },
798
+ forward() {
799
+ window.history.forward();
800
+ },
801
+ prefetch(href) {
802
+ router.prefetch(href);
803
+ }
804
+ };
805
+ }
806
+ //#endregion
807
+ //#region src/client/use-pathname.ts
808
+ /**
809
+ * usePathname() — client-side hook for reading the current pathname.
810
+ *
811
+ * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').
812
+ * Updates when client-side navigation changes the URL.
813
+ *
814
+ * This is a thin wrapper over window.location.pathname, provided for
815
+ * Next.js API compatibility (libraries like nuqs import usePathname
816
+ * from next/navigation).
817
+ *
818
+ * During SSR, reads the request pathname from the SSR ALS context
819
+ * (populated by ssr-entry.ts) instead of window.location.
820
+ */
821
+ function getPathname() {
822
+ if (typeof window !== "undefined") return window.location.pathname;
823
+ return getSsrData()?.pathname ?? "/";
824
+ }
825
+ function getServerPathname() {
826
+ return getSsrData()?.pathname ?? "/";
827
+ }
828
+ function subscribe$1(callback) {
829
+ window.addEventListener("popstate", callback);
830
+ return () => window.removeEventListener("popstate", callback);
831
+ }
832
+ /**
833
+ * Read the current URL pathname.
834
+ *
835
+ * Compatible with Next.js's `usePathname()` from `next/navigation`.
836
+ */
837
+ function usePathname() {
838
+ return useSyncExternalStore(subscribe$1, getPathname, getServerPathname);
839
+ }
840
+ //#endregion
841
+ //#region src/client/use-search-params.ts
842
+ /**
843
+ * useSearchParams() — client-side hook for reading URL search params.
844
+ *
845
+ * Returns a read-only URLSearchParams instance reflecting the current
846
+ * URL's query string. Updates when client-side navigation changes the URL.
847
+ *
848
+ * This is a thin wrapper over window.location.search, provided for
849
+ * Next.js API compatibility (libraries like nuqs import useSearchParams
850
+ * from next/navigation).
851
+ *
852
+ * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
853
+ * URLSearchParams. Mutation methods (set, delete, append) work on the
854
+ * local copy but do NOT affect the URL — use the router or nuqs for that.
855
+ *
856
+ * During SSR, reads the request search params from the SSR ALS context
857
+ * (populated by ssr-entry.ts) instead of window.location.
858
+ */
859
+ function getSearch() {
860
+ if (typeof window !== "undefined") return window.location.search;
861
+ const data = getSsrData();
862
+ if (!data) return "";
863
+ const str = new URLSearchParams(data.searchParams).toString();
864
+ return str ? `?${str}` : "";
865
+ }
866
+ function getServerSearch() {
867
+ const data = getSsrData();
868
+ if (!data) return "";
869
+ const str = new URLSearchParams(data.searchParams).toString();
870
+ return str ? `?${str}` : "";
871
+ }
872
+ function subscribe(callback) {
873
+ window.addEventListener("popstate", callback);
874
+ return () => window.removeEventListener("popstate", callback);
875
+ }
876
+ var cachedSearch = "";
877
+ var cachedParams = new URLSearchParams();
878
+ function getSearchParams() {
879
+ const search = getSearch();
880
+ if (search !== cachedSearch) {
881
+ cachedSearch = search;
882
+ cachedParams = new URLSearchParams(search);
883
+ }
884
+ return cachedParams;
885
+ }
886
+ function getServerSearchParams() {
887
+ const data = getSsrData();
888
+ return data ? new URLSearchParams(data.searchParams) : new URLSearchParams();
889
+ }
890
+ /**
891
+ * Read the current URL search params.
892
+ *
893
+ * Compatible with Next.js's `useSearchParams()` from `next/navigation`.
894
+ */
895
+ function useSearchParams() {
896
+ useSyncExternalStore(subscribe, getSearch, getServerSearch);
897
+ return typeof window !== "undefined" ? getSearchParams() : getServerSearchParams();
898
+ }
899
+ //#endregion
900
+ //#region src/client/segment-context.ts
901
+ /**
902
+ * Segment Context — provides layout segment position for useSelectedLayoutSegment hooks.
903
+ *
904
+ * Each layout in the segment tree is wrapped with a SegmentProvider that stores
905
+ * the URL segments from root to the current layout level. The hooks read this
906
+ * context to determine which child segments are active below the calling layout.
907
+ *
908
+ * The context value is intentionally minimal: just the segment path array and
909
+ * parallel route keys. No internal cache details are exposed.
910
+ *
911
+ * Design docs: design/19-client-navigation.md, design/14-ecosystem.md
912
+ */
913
+ var SegmentContext = createContext(null);
914
+ /** Read the segment context. Returns null if no provider is above this component. */
915
+ function useSegmentContext() {
916
+ return useContext(SegmentContext);
917
+ }
918
+ /**
919
+ * Wraps each layout to provide segment position context.
920
+ * Injected by rsc-entry.ts during element tree construction.
921
+ */
922
+ function SegmentProvider({ segments, parallelRouteKeys, children }) {
923
+ const value = useMemo(() => ({
924
+ segments,
925
+ parallelRouteKeys
926
+ }), [segments.join("/"), parallelRouteKeys.join(",")]);
927
+ return createElement(SegmentContext.Provider, { value }, children);
928
+ }
929
+ //#endregion
930
+ //#region src/client/use-selected-layout-segment.ts
931
+ /**
932
+ * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks
933
+ * for reading the active segment(s) below the current layout.
934
+ *
935
+ * These hooks are used by navigation UIs to highlight active sections.
936
+ * They match Next.js's API from next/navigation.
937
+ *
938
+ * How they work:
939
+ * 1. Each layout is wrapped with a SegmentProvider that records its depth
940
+ * (the URL segments from root to that layout level).
941
+ * 2. The hooks read the current URL pathname via usePathname().
942
+ * 3. They compare the layout's segment depth against the full URL segments
943
+ * to determine which child segments are "selected" below.
944
+ *
945
+ * Example: For URL "/dashboard/settings/profile"
946
+ * - Root layout (depth 0, segments: ['']): selected segment = "dashboard"
947
+ * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = "settings"
948
+ * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = "profile"
949
+ *
950
+ * Design docs: design/19-client-navigation.md, design/14-ecosystem.md
951
+ */
952
+ /**
953
+ * Split a pathname into URL segments.
954
+ * "/" → [""]
955
+ * "/dashboard" → ["", "dashboard"]
956
+ * "/dashboard/settings" → ["", "dashboard", "settings"]
957
+ */
958
+ function pathnameToSegments(pathname) {
959
+ return pathname.split("/");
960
+ }
961
+ /**
962
+ * Pure function: compute the selected child segment given a layout's segment
963
+ * depth and the current URL pathname.
964
+ *
965
+ * @param contextSegments — segments from root to the calling layout, or null if no context
966
+ * @param pathname — current URL pathname
967
+ * @returns the active child segment one level below, or null if at the leaf
968
+ */
969
+ function getSelectedSegment(contextSegments, pathname) {
970
+ const urlSegments = pathnameToSegments(pathname);
971
+ if (!contextSegments) return urlSegments[1] || null;
972
+ return urlSegments[contextSegments.length] || null;
973
+ }
974
+ /**
975
+ * Pure function: compute all selected segments below a layout's depth.
976
+ *
977
+ * @param contextSegments — segments from root to the calling layout, or null if no context
978
+ * @param pathname — current URL pathname
979
+ * @returns all active segments below the layout
980
+ */
981
+ function getSelectedSegments(contextSegments, pathname) {
982
+ const urlSegments = pathnameToSegments(pathname);
983
+ if (!contextSegments) return urlSegments.slice(1).filter(Boolean);
984
+ const depth = contextSegments.length;
985
+ return urlSegments.slice(depth).filter(Boolean);
986
+ }
987
+ /**
988
+ * Returns the active child segment one level below the layout where this
989
+ * hook is called. Returns `null` if the layout is the leaf (no child segment).
990
+ *
991
+ * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.
992
+ *
993
+ * @param parallelRouteKey — Optional parallel route key. Currently unused
994
+ * (parallel route segment tracking is not yet implemented). Accepted for
995
+ * API compatibility with Next.js.
996
+ */
997
+ function useSelectedLayoutSegment(parallelRouteKey) {
998
+ const context = useSegmentContext();
999
+ const pathname = usePathname();
1000
+ return getSelectedSegment(context?.segments ?? null, pathname);
1001
+ }
1002
+ /**
1003
+ * Returns all active segments below the layout where this hook is called.
1004
+ * Returns an empty array if the layout is the leaf (no child segments).
1005
+ *
1006
+ * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.
1007
+ *
1008
+ * @param parallelRouteKey — Optional parallel route key. Currently unused
1009
+ * (parallel route segment tracking is not yet implemented). Accepted for
1010
+ * API compatibility with Next.js.
1011
+ */
1012
+ function useSelectedLayoutSegments(parallelRouteKey) {
1013
+ const context = useSegmentContext();
1014
+ const pathname = usePathname();
1015
+ return getSelectedSegments(context?.segments ?? null, pathname);
1016
+ }
1017
+ //#endregion
1018
+ //#region src/client/form.tsx
1019
+ /**
1020
+ * Client-side form utilities for server actions.
1021
+ *
1022
+ * Exports a typed `useActionState` that understands the action builder's result shape.
1023
+ * Result is typed to:
1024
+ * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null
1025
+ *
1026
+ * The action builder emits a function that satisfies both the direct call signature
1027
+ * and React's `(prevState, formData) => Promise<State>` contract.
1028
+ *
1029
+ * See design/08-forms-and-actions.md §"Client-Side Form Mechanics"
1030
+ */
1031
+ /**
1032
+ * Typed wrapper around React 19's `useActionState` that understands
1033
+ * the timber action builder's result shape.
1034
+ *
1035
+ * @param action - A server action created with createActionClient or a raw 'use server' function.
1036
+ * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS
1037
+ * progressive enhancement — the flash seeds the initial state so the form has a
1038
+ * single source of truth for both with-JS and no-JS paths.
1039
+ * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).
1040
+ *
1041
+ * @example
1042
+ * ```tsx
1043
+ * 'use client'
1044
+ * import { useActionState } from '@timber/app/client'
1045
+ * import { createTodo } from './actions'
1046
+ *
1047
+ * export function NewTodoForm({ flash }) {
1048
+ * const [result, action, isPending] = useActionState(createTodo, flash)
1049
+ * return (
1050
+ * <form action={action}>
1051
+ * <input name="title" />
1052
+ * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}
1053
+ * <button disabled={isPending}>Add</button>
1054
+ * </form>
1055
+ * )
1056
+ * }
1057
+ * ```
1058
+ */
1059
+ function useActionState(action, initialState, permalink) {
1060
+ return useActionState$1(action, initialState, permalink);
1061
+ }
1062
+ /**
1063
+ * Hook for calling a server action imperatively (not via a form).
1064
+ * Returns [execute, isPending] where execute accepts the input directly.
1065
+ *
1066
+ * @example
1067
+ * ```tsx
1068
+ * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)
1069
+ * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>
1070
+ * Delete
1071
+ * </button>
1072
+ * ```
1073
+ */
1074
+ function useFormAction(action) {
1075
+ const [isPending, startTransition] = useTransition();
1076
+ const execute = (input) => {
1077
+ return new Promise((resolve) => {
1078
+ startTransition(async () => {
1079
+ resolve(await action(input));
1080
+ });
1081
+ });
1082
+ };
1083
+ return [execute, isPending];
1084
+ }
1085
+ /**
1086
+ * Extract per-field and form-level errors from an ActionResult.
1087
+ *
1088
+ * Pure function (no internal hooks) — follows React naming convention
1089
+ * since it's used in render. Accepts the result from `useActionState`
1090
+ * or flash data from `getFormFlash()`.
1091
+ *
1092
+ * @example
1093
+ * ```tsx
1094
+ * const [result, action, isPending] = useActionState(createTodo, null)
1095
+ * const errors = useFormErrors(result)
1096
+ *
1097
+ * return (
1098
+ * <form action={action}>
1099
+ * <input name="title" />
1100
+ * {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}
1101
+ * {errors.formErrors.map(e => <p key={e}>{e}</p>)}
1102
+ * </form>
1103
+ * )
1104
+ * ```
1105
+ */
1106
+ function useFormErrors(result) {
1107
+ const empty = {
1108
+ fieldErrors: {},
1109
+ formErrors: [],
1110
+ serverError: null,
1111
+ hasErrors: false,
1112
+ getFieldError: () => null
1113
+ };
1114
+ if (!result) return empty;
1115
+ const validationErrors = result.validationErrors;
1116
+ const serverError = result.serverError;
1117
+ if (!validationErrors && !serverError) return empty;
1118
+ const fieldErrors = {};
1119
+ const formErrors = [];
1120
+ if (validationErrors) for (const [key, messages] of Object.entries(validationErrors)) if (key === "_root") formErrors.push(...messages);
1121
+ else fieldErrors[key] = messages;
1122
+ const hasErrors = Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;
1123
+ return {
1124
+ fieldErrors,
1125
+ formErrors,
1126
+ serverError: serverError ?? null,
1127
+ hasErrors,
1128
+ getFieldError(field) {
1129
+ const errs = fieldErrors[field];
1130
+ return errs && errs.length > 0 ? errs[0] : null;
1131
+ }
1132
+ };
1133
+ }
1134
+ //#endregion
1135
+ //#region src/client/use-query-states.ts
1136
+ /**
1137
+ * useQueryStates — client-side hook for URL-synced search params.
1138
+ *
1139
+ * Delegates to nuqs for URL synchronization, batching, React 19 transitions,
1140
+ * and throttled URL writes. Bridges timber's SearchParamCodec protocol to
1141
+ * nuqs-compatible parsers.
1142
+ *
1143
+ * Design doc: design/23-search-params.md §"Codec Bridge"
1144
+ */
1145
+ /**
1146
+ * Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.
1147
+ *
1148
+ * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }
1149
+ * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }
1150
+ */
1151
+ function bridgeCodec(codec) {
1152
+ return {
1153
+ parse: (v) => codec.parse(v),
1154
+ serialize: (v) => codec.serialize(v) ?? "",
1155
+ defaultValue: codec.parse(void 0),
1156
+ eq: (a, b) => codec.serialize(a) === codec.serialize(b)
1157
+ };
1158
+ }
1159
+ /**
1160
+ * Bridge an entire codec map to nuqs-compatible parsers.
1161
+ */
1162
+ function bridgeCodecs(codecs) {
1163
+ const result = {};
1164
+ for (const key of Object.keys(codecs)) result[key] = bridgeCodec(codecs[key]);
1165
+ return result;
1166
+ }
1167
+ /**
1168
+ * Read and write typed search params from/to the URL.
1169
+ *
1170
+ * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in
1171
+ * browser-entry.ts) handles RSC navigation on non-shallow updates.
1172
+ *
1173
+ * Usage:
1174
+ * ```ts
1175
+ * // Via a SearchParamsDefinition
1176
+ * const [params, setParams] = definition.useQueryStates()
1177
+ *
1178
+ * // Standalone with inline codecs
1179
+ * const [params, setParams] = useQueryStates({
1180
+ * page: fromSchema(z.coerce.number().int().min(1).default(1)),
1181
+ * })
1182
+ * ```
1183
+ */
1184
+ function useQueryStates(codecsOrRoute, _options, urlKeys) {
1185
+ let codecs;
1186
+ let resolvedUrlKeys = urlKeys;
1187
+ if (typeof codecsOrRoute === "string") {
1188
+ const definition = getSearchParams$1(codecsOrRoute);
1189
+ if (!definition) throw new Error(`useQueryStates('${codecsOrRoute}'): no search params registered for this route. Either the route has no search-params.ts file, or it hasn't been loaded yet. For cross-route usage, import the definition explicitly.`);
1190
+ codecs = definition.codecs;
1191
+ resolvedUrlKeys = definition.urlKeys;
1192
+ } else codecs = codecsOrRoute;
1193
+ const bridged = bridgeCodecs(codecs);
1194
+ const nuqsOptions = {};
1195
+ if (resolvedUrlKeys && Object.keys(resolvedUrlKeys).length > 0) nuqsOptions.urlKeys = resolvedUrlKeys;
1196
+ const [values, setValues] = useQueryStates$1(bridged, nuqsOptions);
1197
+ const setParams = (partial, setOptions) => {
1198
+ const nuqsSetOptions = {};
1199
+ if (setOptions?.shallow !== void 0) nuqsSetOptions.shallow = setOptions.shallow;
1200
+ if (setOptions?.scroll !== void 0) nuqsSetOptions.scroll = setOptions.scroll;
1201
+ if (setOptions?.history !== void 0) nuqsSetOptions.history = setOptions.history;
1202
+ setValues(partial, nuqsSetOptions);
1203
+ };
1204
+ return [values, setParams];
1205
+ }
1206
+ /**
1207
+ * Create a useQueryStates binding for a SearchParamsDefinition.
1208
+ * This is used internally by SearchParamsDefinition.useQueryStates().
1209
+ */
1210
+ function bindUseQueryStates(definition) {
1211
+ return (options) => {
1212
+ return useQueryStates(definition.codecs, options, definition.urlKeys);
1213
+ };
1214
+ }
1215
+ //#endregion
1216
+ export { HistoryStack, Link, LinkStatusContext, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, useParams, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
1217
+
1218
+ //# sourceMappingURL=index.js.map