@timber-js/app 0.2.0-alpha.212 → 0.2.0-alpha.214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-CCdnVtWm.js → actions-CUh3cClk.js} +43 -8
- package/dist/_chunks/{actions-CCdnVtWm.js.map → actions-CUh3cClk.js.map} +1 -1
- package/dist/_chunks/{canonicalize-CgHoscYO.js → canonicalize-BAWkWiKK.js} +28 -2
- package/dist/_chunks/{canonicalize-CgHoscYO.js.map → canonicalize-BAWkWiKK.js.map} +1 -1
- package/dist/_chunks/{chains-BfoPFraI.js → chains-CjK1Eu6a.js} +2 -2
- package/dist/_chunks/{chains-BfoPFraI.js.map → chains-CjK1Eu6a.js.map} +1 -1
- package/dist/_chunks/{classify-BT66U83D.js → classify-GAdt6aiA.js} +2 -2
- package/dist/_chunks/{classify-BT66U83D.js.map → classify-GAdt6aiA.js.map} +1 -1
- package/dist/_chunks/{cli-check-BfQ54-UJ.js → cli-check-DOzjlycg.js} +3 -3
- package/dist/_chunks/{cli-check-BfQ54-UJ.js.map → cli-check-DOzjlycg.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-czh2dsLs.js → cli-schema-sync-B9wDaQvW.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-czh2dsLs.js.map → cli-schema-sync-B9wDaQvW.js.map} +1 -1
- package/dist/_chunks/{client-dep-entries-CQwpb8dI.js → client-dep-entries-DyDqXOF9.js} +2 -2
- package/dist/_chunks/{client-dep-entries-CQwpb8dI.js.map → client-dep-entries-DyDqXOF9.js.map} +1 -1
- package/dist/_chunks/{convention-lint-BEVW4EID.js → convention-lint-B3QEGJX7.js} +2 -2
- package/dist/_chunks/{convention-lint-BEVW4EID.js.map → convention-lint-B3QEGJX7.js.map} +1 -1
- package/dist/_chunks/{dev-server-FKxptbnI.js → dev-server-_9L4KWC-.js} +3 -3
- package/dist/_chunks/{dev-server-FKxptbnI.js.map → dev-server-_9L4KWC-.js.map} +1 -1
- package/dist/_chunks/{error-boundary-DsNScGRM.js → error-boundary-xxxLtXt6.js} +38 -5
- package/dist/_chunks/{error-boundary-DsNScGRM.js.map → error-boundary-xxxLtXt6.js.map} +1 -1
- package/dist/_chunks/{live-graph-C_4v-fHv.js → live-graph-Cd3YHvrH.js} +4 -4
- package/dist/_chunks/{live-graph-C_4v-fHv.js.map → live-graph-Cd3YHvrH.js.map} +1 -1
- package/dist/_chunks/{poison-scan-C92liMAr.js → poison-scan-BnJjBOkn.js} +2 -2
- package/dist/_chunks/{poison-scan-C92liMAr.js.map → poison-scan-BnJjBOkn.js.map} +1 -1
- package/dist/_chunks/{scanner-CQt12vE2.js → scanner-CKAT5gRx.js} +2 -2
- package/dist/_chunks/{scanner-CQt12vE2.js.map → scanner-CKAT5gRx.js.map} +1 -1
- package/dist/_chunks/{walkers-DAT4avhZ.js → walkers-DwCXEyRu.js} +3 -3
- package/dist/_chunks/{walkers-DAT4avhZ.js.map → walkers-DwCXEyRu.js.map} +1 -1
- package/dist/adapters/nitro-preview.d.ts.map +1 -1
- package/dist/adapters/nitro.js +11 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +3 -3
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +1 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/form-state.d.ts +22 -0
- package/dist/client/browser-entry/form-state.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +9 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +2 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +90 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +26 -10
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-transition.d.ts +11 -2
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +70 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +3 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -1
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-form-field.d.ts +39 -0
- package/dist/client/use-form-field.d.ts.map +1 -0
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +11 -2
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +22 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +9 -0
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +13 -4
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/form-state-flight.d.ts +32 -0
- package/dist/server/form-state-flight.d.ts.map +1 -0
- package/dist/server/index.js +37 -23
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +54 -3
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +31 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +2 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +6 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +7 -5
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-form-state.d.ts +30 -0
- package/dist/server/ssr-form-state.d.ts.map +1 -0
- package/dist/shared/form-state-flight.d.ts +36 -0
- package/dist/shared/form-state-flight.d.ts.map +1 -0
- package/docs/api/31-api-client.mdx +22 -0
- package/docs/api/34-api-config.mdx +1 -1
- package/docs/learn/08-forms-and-actions.mdx +109 -22
- package/package.json +1 -1
- package/src/adapters/nitro-preview.ts +10 -1
- package/src/client/browser-entry/action-dispatch.ts +34 -10
- package/src/client/browser-entry/action-queue.ts +1 -1
- package/src/client/browser-entry/form-state.ts +48 -0
- package/src/client/browser-entry/hydrate.ts +9 -18
- package/src/client/browser-entry/index.ts +25 -7
- package/src/client/browser-entry/router-init.ts +3 -2
- package/src/client/index.ts +1 -0
- package/src/client/navigation-transition.ts +13 -2
- package/src/client/router-effects.ts +99 -10
- package/src/client/router-pipeline.ts +7 -3
- package/src/client/router-types.ts +12 -1
- package/src/client/router.ts +35 -6
- package/src/client/use-form-field.ts +132 -0
- package/src/config-types.ts +2 -1
- package/src/server/action-client.ts +68 -53
- package/src/server/canonicalize.ts +29 -0
- package/src/server/flight-scripts.ts +13 -0
- package/src/server/form-data.ts +62 -10
- package/src/server/form-state-flight.ts +67 -0
- package/src/server/pipeline-helpers.ts +61 -0
- package/src/server/pipeline.ts +13 -1
- package/src/server/rsc-entry/action-dispatcher.ts +3 -0
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +16 -0
- package/src/server/rsc-entry/ssr-renderer.ts +12 -14
- package/src/server/ssr-bridge-types.ts +7 -6
- package/src/server/ssr-entry.ts +16 -3
- package/src/server/ssr-form-state.ts +58 -0
- package/src/shared/form-state-flight.ts +74 -0
- package/dist/server/form-state-embed.d.ts +0 -32
- package/dist/server/form-state-embed.d.ts.map +0 -1
- package/src/server/form-state-embed.ts +0 -63
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* Bootstrap call order contract:
|
|
16
16
|
*
|
|
17
17
|
* 1. setupServerActions() — register callServer (independent)
|
|
18
|
+
* takeEmbeddedFormState() — only on a page answering a no-JS action:
|
|
19
|
+
* decode its form state, and run 2–7 after
|
|
18
20
|
* 2. createRscPayloadStream() — decode inlined RSC payload
|
|
19
21
|
* 3. createReactRoot() + — the root host the router renders through,
|
|
20
22
|
* createTimberRouter() then the router + Navigation API
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-init.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/router-init.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAc,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAK9B,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,EAAE,gBAAgB,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,gBAAgB,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACjD;;;;;OAKG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,KAAK,IAAI,KAAK,IAAI,CAAC;CAC5F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"router-init.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/router-init.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAc,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAK9B,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,EAAE,gBAAgB,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,gBAAgB,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACjD;;;;;OAKG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,KAAK,IAAI,KAAK,IAAI,CAAC;CAC5F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA2U/E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
|
-
import { n as TimberErrorBoundary, t as DenyAncestryContext } from "../_chunks/error-boundary-
|
|
3
|
+
import { n as TimberErrorBoundary, t as DenyAncestryContext } from "../_chunks/error-boundary-xxxLtXt6.js";
|
|
4
4
|
export { DenyAncestryContext, TimberErrorBoundary };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { usePathname } from './use-pathname.ts';
|
|
|
14
14
|
export { replaceUrl } from './shallow-url.ts';
|
|
15
15
|
export { useSelectedLayoutSegment, useSelectedLayoutSegments, } from './use-selected-layout-segment.ts';
|
|
16
16
|
export { parseFormErrors, useFormAction } from './form.tsx';
|
|
17
|
+
export { useFormField } from './use-form-field.ts';
|
|
17
18
|
export type { FormErrorsResult } from './form.tsx';
|
|
18
19
|
export { useSegmentParams } from './use-segment-params.ts';
|
|
19
20
|
export { useQueryStates } from './use-query-states.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA4CxF,MAAM,WAAW,YAAY;CAG5B;AACD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACxE,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC1E,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAY3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA4CxF,MAAM,WAAW,YAAY;CAG5B;AACD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACxE,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC1E,YAAY,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAY3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { t as getSsrData } from "../_chunks/ssr-data-nA3I70_n.js";
|
|
|
8
8
|
import { t as getLinkCodec } from "../_chunks/codec-registry-oOUxugz3.js";
|
|
9
9
|
import { i as useNavigationContext, n as useSegmentContext } from "../_chunks/segment-context-xtPUGfdq.js";
|
|
10
10
|
import { n as useQueryStates } from "../_chunks/use-query-states-I3JMng6J.js";
|
|
11
|
-
import React, { createContext, useContext, useRef, useState, useTransition } from "react";
|
|
11
|
+
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, useTransition } from "react";
|
|
12
12
|
import { jsx } from "react/jsx-runtime";
|
|
13
13
|
//#region src/client/use-link-status.ts
|
|
14
14
|
/**
|
|
@@ -652,6 +652,94 @@ function parseFormErrors(result) {
|
|
|
652
652
|
};
|
|
653
653
|
}
|
|
654
654
|
//#endregion
|
|
655
|
+
//#region src/client/use-form-field.ts
|
|
656
|
+
/**
|
|
657
|
+
* useFormField — state for a form field that needs JavaScript (a combobox, a
|
|
658
|
+
* chip list, a row list), made to behave like a native uncontrolled input.
|
|
659
|
+
*
|
|
660
|
+
* React resets a form after its action settles, so a native field with a
|
|
661
|
+
* `defaultValue` lands on whatever default the page now renders: the action's
|
|
662
|
+
* `submittedValues`, or fresh page data after a redirect. State held in
|
|
663
|
+
* `useState` misses that reset and keeps showing the pre-submit edit. This
|
|
664
|
+
* hook shows `defaultValue` until the user edits, and drops the edit when the
|
|
665
|
+
* form resets, so it lands where the native fields do.
|
|
666
|
+
*
|
|
667
|
+
* See design/08-forms-and-actions.md §"The Form Model".
|
|
668
|
+
*/
|
|
669
|
+
/**
|
|
670
|
+
* State for a field that follows its form's reset.
|
|
671
|
+
*
|
|
672
|
+
* Returns `[value, setValue, ref]`. Attach `ref` to any element inside the
|
|
673
|
+
* `<form>`, and render the value into the form data yourself (usually a
|
|
674
|
+
* hidden input). `value` is `defaultValue` until `setValue` is called, and is
|
|
675
|
+
* `defaultValue` again after the form resets, unless the form cancels its
|
|
676
|
+
* `reset` event. An edit dispatches a bubbling `change` event from the `ref`
|
|
677
|
+
* element after it commits, so a native `change` listener on the form sees it
|
|
678
|
+
* as it sees a native input's (React's synthetic `onChange` reports only
|
|
679
|
+
* inputs, selects and textareas). A reset and a new default fire none, as
|
|
680
|
+
* they fire none for a native input.
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* ```tsx
|
|
684
|
+
* // draft: the form's defaults, read from result?.submittedValues ?? pageData
|
|
685
|
+
* const [tags, setTags, ref] = useFormField(draft.tags);
|
|
686
|
+
* <div ref={ref}>
|
|
687
|
+
* <input type="hidden" name="tags" value={tags.join(',')} />
|
|
688
|
+
* <TagPicker value={tags} onChange={setTags} />
|
|
689
|
+
* </div>
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
function useFormField(defaultValue) {
|
|
693
|
+
const [edit, setEdit] = useState(null);
|
|
694
|
+
const value = edit ? edit.value : defaultValue;
|
|
695
|
+
const defaultRef = useRef(defaultValue);
|
|
696
|
+
useLayoutEffect(() => {
|
|
697
|
+
defaultRef.current = defaultValue;
|
|
698
|
+
});
|
|
699
|
+
const pendingResets = useRef([]);
|
|
700
|
+
const takeResets = useCallback(() => {
|
|
701
|
+
const over = pendingResets.current.filter((e) => e.eventPhase === Event.NONE);
|
|
702
|
+
if (over.length === 0) return false;
|
|
703
|
+
pendingResets.current = pendingResets.current.filter((e) => !over.includes(e));
|
|
704
|
+
return over.some((e) => !e.defaultPrevented);
|
|
705
|
+
}, []);
|
|
706
|
+
const setValue = useCallback((next) => {
|
|
707
|
+
const wasReset = takeResets();
|
|
708
|
+
setEdit((latest) => {
|
|
709
|
+
const current = wasReset ? null : latest;
|
|
710
|
+
const base = current ? current.value : defaultRef.current;
|
|
711
|
+
const value = next instanceof Function ? next(base) : next;
|
|
712
|
+
return Object.is(value, base) ? current : { value };
|
|
713
|
+
});
|
|
714
|
+
}, [takeResets]);
|
|
715
|
+
const element = useRef(null);
|
|
716
|
+
const ref = useCallback((el) => {
|
|
717
|
+
element.current = el;
|
|
718
|
+
const form = el?.closest("form");
|
|
719
|
+
if (!form) return;
|
|
720
|
+
const onReset = (event) => {
|
|
721
|
+
pendingResets.current.push(event);
|
|
722
|
+
queueMicrotask(() => {
|
|
723
|
+
if (takeResets()) setEdit(null);
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
form.addEventListener("reset", onReset);
|
|
727
|
+
return () => {
|
|
728
|
+
element.current = null;
|
|
729
|
+
pendingResets.current = [];
|
|
730
|
+
form.removeEventListener("reset", onReset);
|
|
731
|
+
};
|
|
732
|
+
}, [takeResets]);
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
if (edit) element.current?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
735
|
+
}, [edit]);
|
|
736
|
+
return [
|
|
737
|
+
value,
|
|
738
|
+
setValue,
|
|
739
|
+
ref
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
//#endregion
|
|
655
743
|
//#region src/client/params-context.ts
|
|
656
744
|
/**
|
|
657
745
|
* Segment params context — the one channel params use to reach the browser.
|
|
@@ -741,6 +829,6 @@ function useSegmentParams(segmentPath) {
|
|
|
741
829
|
return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);
|
|
742
830
|
}
|
|
743
831
|
//#endregion
|
|
744
|
-
export { Link, LinkStatusContext, buildLinkProps, interpolateParams, mergePreservedSearchParams, parseFormErrors, replaceUrl, resolveHref, useFormAction, useLinkStatus, usePathname, usePendingNavigation, useQueryStates, useRouter, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateNavigationHref as validateLinkHref };
|
|
832
|
+
export { Link, LinkStatusContext, buildLinkProps, interpolateParams, mergePreservedSearchParams, parseFormErrors, replaceUrl, resolveHref, useFormAction, useFormField, useLinkStatus, usePathname, usePendingNavigation, useQueryStates, useRouter, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateNavigationHref as validateLinkHref };
|
|
745
833
|
|
|
746
834
|
//# sourceMappingURL=index.js.map
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/location-search.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\n\n/**\n * The app-visible search string from the address bar.\n *\n * Mirrors what `appVisibleSearch()` does on the server: strips key-shaped\n * `_rsc` values so the client and server agree on search state regardless\n * of whether the document URL itself carries a payload cache key (a user\n * pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an\n * application legitimately owning that name — survive, using the same\n * `isRscCacheKeyShape` predicate the server uses.\n */\nexport function locationSearch(): string {\n return stripRscCacheKey(window.location.search);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop is a query string (from `definition.buildSearchParams()`)\n// or a plain object whose values are String()-coerced\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { LinkFunction } from './index.ts';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.ts';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.ts';\nimport { LinkStatusContext } from './use-link-status.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { getSsrData } from './ssr-data.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { getLinkCodec } from '../params/codec-registry.ts';\nimport { locationSearch } from './location-search.ts';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { LinkStatus } from './use-link-status.ts';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads the raw\n * query string from the request context (getSsrData) — the same `''` or\n * `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the\n * server-rendered href matches what the hydrated client rebuilds from the\n * address bar (TIM-1428). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return locationSearch();\n return getSsrData()?.search ?? '';\n}\n\n/** Native in-page scrolling needs neither an RSC navigation nor a prefetch. */\nfunction isNativeFragmentLink(anchor: HTMLAnchorElement): boolean {\n // Default browser navigation follows the rendered anchor, which may differ\n // from a freshly resolved preserveSearchParams URL after a shallow update.\n const href = anchor.href;\n const hashIndex = href.indexOf('#');\n // Compare browser-serialized URLs, excluding only fragments. URL.hash and\n // URL.search erase the distinction between absent and explicitly empty\n // delimiters, but '/current' and '/current?' are different documents.\n return hashIndex !== -1 && href.slice(0, hashIndex) === window.location.href.split('#', 1)[0];\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Replace the current history entry instead of pushing a new one, so Back\n * skips the page the link was on.\n */\n replace?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n /**\n * View transition types to add to this link's navigation, beside the\n * router's own `navigation-forward`. A `<ViewTransition>` in the\n * destination (or the page being left) can key its animation on them:\n *\n * ```tsx\n * <Link href=\"/products/1\" transitionTypes={['to-detail']}>…</Link>\n * ```\n *\n * The router adds them in the transition that renders the destination. A\n * `startTransition(() => { addTransitionType(t); router.push(href) })` in\n * an `onClick` does not work in timber: the destination reaches React only\n * after its fetch, in a later transition, and React hands types added in\n * the click to whatever transition commits next — not to that one\n * (design/37-navigation-api.md §\"Transition types\").\n */\n transitionTypes?: readonly string[];\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Two shapes, discriminated at runtime by `typeof === 'string'`:\n//\n// 1. Codec-aware (preferred) — a query string built from a definition:\n// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}\n// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,\n// and omits values equal to their default. It is typed `Partial<T>` at\n// the call site, so the definition supplies the type checking.\n//\n// 2. Plain object (escape hatch) — every value is String()-coerced:\n// searchParams={{ ref: 'email' }}\n// No codecs, no aliases. This is for one-off params that have no\n// definition. Passing a *definition's* keys this way is a mistake the\n// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the\n// definition would emit `?q=x`. Prefer (1) whenever a definition exists.\n//\n// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a\n// Server Component, so this prop crosses the RSC Flight boundary before the\n// client Link runs. `URLSearchParams` is iterable, and React serializes any\n// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into\n// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight\n// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)\n//\n// TIM-1343 removed the third shape — a flat values object resolved against a\n// runtime registry keyed by href. It required scanning a `params.ts`\n// convention file, a generated registry module, and eager imports of every\n// route's definition into all three entries, to save one `.buildSearchParams`\n// call. See design/23-search-params.md §\"Link Integration\".\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype LinkSearchParamsProp = string | Record<string, unknown>;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n const encoded = encodeURIComponent(str);\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n return prefix + encoded + suffix;\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization (see the two shapes documented above)\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers\n * hand-rolling a query string reasonably might, and silently producing\n * `?%3Fa=b` for it would be a worse failure than accepting both.\n */\nfunction stripLeadingQuestionMark(qs: string): string {\n return qs.startsWith('?') ? qs.slice(1) : qs;\n}\n\n/**\n * Escape-hatch serialization for a plain object: String()-coerce every\n * value, append arrays as repeated keys, skip null/undefined.\n *\n * Deliberately codec-free and alias-free — see the shape docs above.\n */\nfunction coerceToQueryString(values: Record<string, unknown>): string {\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n return usp.toString();\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n // A string is already a serialized query (from\n // `definition.buildSearchParams()`), so it passes through untouched.\n // `typeof` is the only discriminator that survives the RSC Flight\n // boundary — see the shape docs above for what happened when this was\n // an object test.\n const qs =\n typeof searchParams === 'string'\n ? stripLeadingQuestionMark(searchParams)\n : coerceToQueryString(searchParams);\n\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n replace,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n transitionTypes,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Local `useState`, deliberately NOT `useTransition` (TIM-1307).\n //\n // Either shape keeps the re-render local — the state lives on this Link's\n // own fiber, so no sibling link is touched. The difference is what wrapping\n // `router.navigate()` in a transition does to the REST of the app: returning\n // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,\n // which calls react-dom's `entangleAsyncAction`. That opens an action scope\n // whose `currentEntangledLane` collects every transition update scheduled\n // while it is open — including the router's `root.render` of `NavigationRoot`, which is a\n // fully synchronous `startTransition` in a different component — and\n // rendering that lane suspends on `currentEntangledActionThenable` until the\n // action settles. `navigateTransition` awaits `decodePromise`, so a Link\n // navigation used to commit only after the whole Flight stream had decoded,\n // while `useRouter().push()` committed as soon as React could render it.\n //\n // Two commit-timing regimes, and the slower one was the dominant path: no\n // streaming reveal, and the TIM-1301 publish (address bar, segment cache,\n // `timber:navigation-end`) waited for full decode. Not opening an action\n // scope is what collapses them into one.\n //\n // `isPending` runs from the click until BOTH `router.navigate()` has settled\n // (after `decodePromise` — the lifecycle the router's pending store and the\n // TopLoader use) AND the navigation's `onCommit` has fired. The second\n // condition is what keeps the flag honest: the promise resolves on decode,\n // and a destination with a pending Suspense boundary is still off screen\n // then. A clear scheduled at that point — urgent or in its own transition —\n // can commit ahead of the suspended tree, so the link flashes idle for a\n // frame while the old page is still showing (TIM-1418). Wrapping the clear\n // in `startTransition` was tried: it only holds when both updates share a\n // lane, and React assigns lanes per event, so the decode-time clear and the\n // click-time `root.render` never do. (Next.js gets the shared lane because\n // it hands React the tree in the click event; timber fetches first.) The\n // commit itself is the only signal that cannot beat the commit, and it is\n // the router's to give: `onCommit` fires exactly once, on the commit or\n // when the navigation is abandoned — superseded or failed — so the link\n // can never be left pending for a commit that will not come.\n //\n // Order is not fixed: a destination that does not suspend commits before\n // decode finishes (streaming reveal), so whichever of settle/commit comes\n // second clears.\n //\n // `clickSeq` guards the same-link double click: the first navigation is\n // superseded (its promise RESOLVES — `runNavigation` swallows AbortErrors —\n // and its `onCommit` fires) while the second is still in flight, and only\n // the newest click for this link may clear its flag. Written on click and\n // nowhere else, so there is no reset for a settle handler to race.\n const [isPending, setIsPending] = useState(false);\n const clickSeq = useRef(0);\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // Read via getCurrentSearch() rather than a hook, to avoid an\n // unconditional hook call for a prop most links don't pass. On the\n // client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's raw query string.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // Event callbacks may update the URL, so resolve preserved params at use time.\n const resolveEventUrl = () =>\n new URL(\n preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref,\n window.location.href\n );\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Native anchor scrolling is not an SPA navigation. Decide before\n // onNavigate can cancel it, just as we do before hover prefetching.\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n // Keep the post-callback read: onNavigate may change the current URL\n // and therefore the search params to preserve or fragment locality.\n const resolved = resolveEventUrl();\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;\n\n const seq = ++clickSeq.current;\n let settled = false;\n let committed = false;\n const clear = () => {\n if (clickSeq.current === seq) setIsPending(false);\n };\n const onCommit = () => {\n committed = true;\n if (settled) clear();\n };\n const settle = () => {\n settled = true;\n if (committed) clear();\n };\n setIsPending(true);\n const navigation = router.navigate(absoluteHref, {\n scroll: shouldScroll,\n replace,\n transitionTypes,\n onCommit,\n });\n navigation.then(settle, (error: unknown) => {\n clear();\n // Rethrow, so a navigation error that the router did not already\n // recover from surfaces as an unhandled rejection — the same\n // regime as `useRouter().push()`'s `void router.navigate(...)`.\n //\n // It used to reach the nearest error boundary instead: React\n // re-throws a rejected async action during render (measured on\n // 19.2.7). That replaced the departing page with the app's error\n // UI, which contradicts what every other failure path here\n // promises — a navigation that fails leaves the user on the page\n // they were already looking at (TIM-1306). Recoverable failures\n // never get here anyway; `runNavigation` swallows AbortErrors and\n // `recoverFromNavigationError` turns a failed fetch into a full\n // document load.\n throw error;\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n if (isNativeFragmentLink(event.currentTarget)) return;\n const resolved = resolveEventUrl();\n router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (pathname, search) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.ts';\nimport { validateNavigationHref } from '../shared/href-validation.ts';\n\n/** Options for `push` and `replace`. */\nexport interface NavigateOptions {\n /** Set to false to keep the scroll position instead of scrolling to top. */\n scroll?: boolean;\n /**\n * View transition types to add to this navigation, beside the router's own\n * `navigation-forward`. See `NavigationOptions.transitionTypes` — they must\n * go here rather than in a `startTransition` around the call.\n */\n transitionTypes?: readonly string[];\n}\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: NavigateOptions): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: NavigateOptions): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n transitionTypes: options?.transitionTypes,\n });\n },\n replace(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n replace: true,\n transitionTypes: options?.transitionTypes,\n });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * One unconditional read of NavigationContext, on every side (TIM-1425):\n *\n * - In the browser, the provider wraps the RSC payload in the router's `renderTree`, so\n * the pathname updates in the same render pass as the new tree.\n * - During SSR, the wrapper chain mounts the same provider with the request's\n * pathname (TIM-1424), so this is the identical code path — no ALS read,\n * no fallback tiers.\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts, which has a throwing stub\n * (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error — loud, not guessed-at from window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { useNavigationContext } from './navigation-context.ts';\n\n/**\n * Read the current URL pathname.\n *\n * Throws when no NavigationProvider is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n */\nexport function usePathname(): string {\n const nav = useNavigationContext();\n if (nav === null) {\n throw new Error(\n '[timber] usePathname() was called outside the timber app tree ' +\n '(no NavigationProvider found). In tests, render the component ' +\n 'inside the timber providers.'\n );\n }\n return nav.pathname;\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, a single\n * navigate event listener replaces the popstate handler and routes plain\n * `<a>` clicks through the router:\n * - Traversals are intercepted, with the event's AbortSignal linked to the\n * router's fetch\n * - Cross-document push/replace navigations are cancelled and re-run through\n * the router, so the URL commits with the tree (never at intercept time)\n * - Per-entry scroll state via NavigationHistoryEntry.getState()\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.tsx';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { TraverseDirection } from './router-types.ts';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Traverse Direction ──────────────────────────────────────────\n\n/**\n * Which way a traversal goes, as the view transition type the router adds\n * for it (design/37-navigation-api.md §\"Transition types\").\n *\n * The Navigation API numbers the session's entries, so a traversal to a\n * lower index is back and to a higher one is forward — including a jump of\n * several entries (`history.go(-3)`). An index of -1 means the entry is not\n * in this document's list, and then the direction is unknown.\n */\nexport function traverseDirection(\n destinationIndex: number,\n currentIndex: number | undefined\n): TraverseDirection {\n if (currentIndex === undefined || currentIndex < 0 || destinationIndex < 0) {\n return 'navigation-traverse';\n }\n if (destinationIndex < currentIndex) return 'navigation-back';\n if (destinationIndex > currentIndex) return 'navigation-forward';\n return 'navigation-traverse';\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * Push/replace navigations are handed to the router, which commits the URL\n * with the tree. Traversals are intercepted and replayed or fetched.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a cross-document push/replace navigation the router did not start\n * itself: a plain `<a>`, `navigation.navigate()`, or `location.assign()`.\n * The handler has already cancelled the browser's navigation, so the\n * address bar has not moved; the router moves it when the destination\n * commits, as it does for its own navigations.\n */\n onExternalNavigate: (url: string, options: { replace: boolean }) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch. `direction` is\n * the view transition type the render adds — see `traverseDirection`.\n */\n onTraverse: (\n url: string,\n scrollY: number,\n signal: AbortSignal,\n direction: TraverseDirection\n ) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi.\n */\nexport interface NavigationApiController {\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * The address bar moves only when the destination's tree commits\n * (design/19-client-navigation.md §\"prepareNavigation\"). `event.intercept()`\n * would commit the URL as soon as it is called, a full round trip before the\n * page that belongs to it, so push/replace navigations are never\n * intercepted: the router's own navigations never reach here as\n * cross-document events (Link cancels the click and the router commits with\n * `pushState`), and the rest are cancelled and re-run through the router.\n * Only traversals — which the browser has already moved the URL for in every\n * browser — and shallow updates are intercepted.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + stripRscCacheKey(destUrl.search);\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n // Read before intercept(): the current entry is still the one the\n // user is leaving.\n const direction = traverseDirection(event.destination.index, nav.currentEntry?.index);\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal, direction);\n },\n });\n return;\n }\n\n // A same-document push/replace is `history.pushState()` /\n // `replaceState()`: the router's own commit, or app code (nuqs,\n // replaceUrl, a third-party library). The URL is already the\n // destination and the History API patch in router-init syncs the search\n // params, exactly as in browsers without the Navigation API.\n if (event.destination.sameDocument) return;\n\n // A cross-document push/replace the router did not start: a plain\n // `<a>`, `navigation.navigate()`, `location.assign()`. Cancel it and\n // re-run it through the router, which commits the URL with the tree.\n // A navigation the browser will not let us cancel stays a document load.\n // Cancelling rejects a `navigation.navigate()` caller's `committed` and\n // `finished` with AbortError and drops its `state`; the router's own API\n // is `useRouter()` (design/19 §\"The address bar moves on commit\").\n if (!event.cancelable) return;\n event.preventDefault();\n void callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n });\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.ts';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.ts';\nimport { usePathname } from './use-pathname.ts';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Forms use React's own `useActionState`: an action built with\n * `createActionClient` already has the `(prevState, payload)` signature it\n * calls, and the result is typed from it. `parseFormErrors` reads the errors\n * out of that result.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useTransition } from 'react';\nimport type {\n ActionFn,\n ActionResult,\n InputHint,\n ValidationErrors,\n} from '../server/action-client.ts';\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve, reject) => {\n startTransition(async () => {\n // A raw server function that throws rejects (TIM-1570). The caller\n // awaiting `execute` gets that rejection; without the catch the\n // promise would never settle.\n try {\n resolve(\n await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n )\n );\n } catch (error) {\n reject(error);\n }\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** What `parseFormErrors` reads out of an action result. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Read the errors out of an action result — the state `useActionState`\n * returns. `_root` validation errors are form-level; every other key is a\n * field.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = parseFormErrors(result)\n * errors.getFieldError('title') // first message for the field, or null\n * ```\n */\nexport function parseFormErrors<TData>(\n result: ActionResult<TData> | null | undefined\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors: ValidationErrors | undefined = result.validationErrors;\n const serverError = result.serverError;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n","/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { toNullProtoRecord, type CoercedParams } from '../shared/param-value.ts';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call throws the outside-the-timber-app-tree error\n * even though the provider is mounted (the module-snapshot fallback it once\n * silently landed on was deleted in TIM-1425).\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from every entry a Server Component imports, so the named form crashed\n * those entries outright (codex, PR #992; originally reproduced against\n * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —\n * the hazard is unchanged for the entries that remain).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null only when no provider\n * is above the caller — a component rendered outside a timber route. During\n * SSR the wrapper chain mounts `PayloadRoot` too (TIM-1424), so both sides\n * resolve through this context.\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: CoercedParams;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * This used to also write a module-level snapshot during render, as the\n * fallback for `useSegmentParams()` called outside a component. That tier is\n * gone (TIM-1425) — the provider is unconditional on every render path,\n * browser and SSR alike, so the hook reads context or throws. Removing the\n * write also removes render-phase shared mutation from the SSR environment,\n * where concurrent requests rendered through this component.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * One unconditional read of ParamsContext, on every side (TIM-1425):\n *\n * - In the browser, `PayloadRoot` publishes the payload's params above the\n * merge point on every render path. Params update atomically with the RSC\n * tree — no timing gap (TIM-1294, TIM-1297).\n * - During SSR, the wrapper chain mounts the same `PayloadRoot`, fed the\n * `params` half of `splitPayloadRoot(root)` — the identical derivation\n * the browser performs at hydration (TIM-1424).\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error.\n *\n * The module-level subscribe/notify machinery and the `currentParams`\n * snapshot that used to back a fourth fallback tier are gone (TIM-1425):\n * the provider is unconditional on every render path, so nothing read them.\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { Routes } from '../index.ts';\nimport { resolveSegmentParams } from '../shared/slot-params.ts';\nimport { useParamsContext } from './params-context.ts';\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * Throws when no `PayloadRoot` is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams {\n const paramsContext = useParamsContext();\n if (paramsContext === null) {\n throw new Error(\n '[timber] useSegmentParams() was called outside the timber app tree ' +\n '(no params provider found). In tests, render the component inside ' +\n 'the timber providers.'\n );\n }\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/location-search.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/use-form-field.ts","../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\n\n/**\n * The app-visible search string from the address bar.\n *\n * Mirrors what `appVisibleSearch()` does on the server: strips key-shaped\n * `_rsc` values so the client and server agree on search state regardless\n * of whether the document URL itself carries a payload cache key (a user\n * pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an\n * application legitimately owning that name — survive, using the same\n * `isRscCacheKeyShape` predicate the server uses.\n */\nexport function locationSearch(): string {\n return stripRscCacheKey(window.location.search);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop is a query string (from `definition.buildSearchParams()`)\n// or a plain object whose values are String()-coerced\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { LinkFunction } from './index.ts';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.ts';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.ts';\nimport { LinkStatusContext } from './use-link-status.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { getSsrData } from './ssr-data.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { getLinkCodec } from '../params/codec-registry.ts';\nimport { locationSearch } from './location-search.ts';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { LinkStatus } from './use-link-status.ts';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads the raw\n * query string from the request context (getSsrData) — the same `''` or\n * `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the\n * server-rendered href matches what the hydrated client rebuilds from the\n * address bar (TIM-1428). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return locationSearch();\n return getSsrData()?.search ?? '';\n}\n\n/** Native in-page scrolling needs neither an RSC navigation nor a prefetch. */\nfunction isNativeFragmentLink(anchor: HTMLAnchorElement): boolean {\n // Default browser navigation follows the rendered anchor, which may differ\n // from a freshly resolved preserveSearchParams URL after a shallow update.\n const href = anchor.href;\n const hashIndex = href.indexOf('#');\n // Compare browser-serialized URLs, excluding only fragments. URL.hash and\n // URL.search erase the distinction between absent and explicitly empty\n // delimiters, but '/current' and '/current?' are different documents.\n return hashIndex !== -1 && href.slice(0, hashIndex) === window.location.href.split('#', 1)[0];\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Replace the current history entry instead of pushing a new one, so Back\n * skips the page the link was on.\n */\n replace?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n /**\n * View transition types to add to this link's navigation, beside the\n * router's own `navigation-forward`. A `<ViewTransition>` in the\n * destination (or the page being left) can key its animation on them:\n *\n * ```tsx\n * <Link href=\"/products/1\" transitionTypes={['to-detail']}>…</Link>\n * ```\n *\n * The router adds them in the transition that renders the destination. A\n * `startTransition(() => { addTransitionType(t); router.push(href) })` in\n * an `onClick` does not work in timber: the destination reaches React only\n * after its fetch, in a later transition, and React hands types added in\n * the click to whatever transition commits next — not to that one\n * (design/37-navigation-api.md §\"Transition types\").\n */\n transitionTypes?: readonly string[];\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Two shapes, discriminated at runtime by `typeof === 'string'`:\n//\n// 1. Codec-aware (preferred) — a query string built from a definition:\n// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}\n// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,\n// and omits values equal to their default. It is typed `Partial<T>` at\n// the call site, so the definition supplies the type checking.\n//\n// 2. Plain object (escape hatch) — every value is String()-coerced:\n// searchParams={{ ref: 'email' }}\n// No codecs, no aliases. This is for one-off params that have no\n// definition. Passing a *definition's* keys this way is a mistake the\n// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the\n// definition would emit `?q=x`. Prefer (1) whenever a definition exists.\n//\n// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a\n// Server Component, so this prop crosses the RSC Flight boundary before the\n// client Link runs. `URLSearchParams` is iterable, and React serializes any\n// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into\n// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight\n// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)\n//\n// TIM-1343 removed the third shape — a flat values object resolved against a\n// runtime registry keyed by href. It required scanning a `params.ts`\n// convention file, a generated registry module, and eager imports of every\n// route's definition into all three entries, to save one `.buildSearchParams`\n// call. See design/23-search-params.md §\"Link Integration\".\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype LinkSearchParamsProp = string | Record<string, unknown>;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n const encoded = encodeURIComponent(str);\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n return prefix + encoded + suffix;\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization (see the two shapes documented above)\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers\n * hand-rolling a query string reasonably might, and silently producing\n * `?%3Fa=b` for it would be a worse failure than accepting both.\n */\nfunction stripLeadingQuestionMark(qs: string): string {\n return qs.startsWith('?') ? qs.slice(1) : qs;\n}\n\n/**\n * Escape-hatch serialization for a plain object: String()-coerce every\n * value, append arrays as repeated keys, skip null/undefined.\n *\n * Deliberately codec-free and alias-free — see the shape docs above.\n */\nfunction coerceToQueryString(values: Record<string, unknown>): string {\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n return usp.toString();\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n // A string is already a serialized query (from\n // `definition.buildSearchParams()`), so it passes through untouched.\n // `typeof` is the only discriminator that survives the RSC Flight\n // boundary — see the shape docs above for what happened when this was\n // an object test.\n const qs =\n typeof searchParams === 'string'\n ? stripLeadingQuestionMark(searchParams)\n : coerceToQueryString(searchParams);\n\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n replace,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n transitionTypes,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Local `useState`, deliberately NOT `useTransition` (TIM-1307).\n //\n // Either shape keeps the re-render local — the state lives on this Link's\n // own fiber, so no sibling link is touched. The difference is what wrapping\n // `router.navigate()` in a transition does to the REST of the app: returning\n // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,\n // which calls react-dom's `entangleAsyncAction`. That opens an action scope\n // whose `currentEntangledLane` collects every transition update scheduled\n // while it is open — including the router's `root.render` of `NavigationRoot`, which is a\n // fully synchronous `startTransition` in a different component — and\n // rendering that lane suspends on `currentEntangledActionThenable` until the\n // action settles. `navigateTransition` awaits `decodePromise`, so a Link\n // navigation used to commit only after the whole Flight stream had decoded,\n // while `useRouter().push()` committed as soon as React could render it.\n //\n // Two commit-timing regimes, and the slower one was the dominant path: no\n // streaming reveal, and the TIM-1301 publish (address bar, segment cache,\n // `timber:navigation-end`) waited for full decode. Not opening an action\n // scope is what collapses them into one.\n //\n // `isPending` runs from the click until BOTH `router.navigate()` has settled\n // (after `decodePromise` — the lifecycle the router's pending store and the\n // TopLoader use) AND the navigation's `onCommit` has fired. The second\n // condition is what keeps the flag honest: the promise resolves on decode,\n // and a destination with a pending Suspense boundary is still off screen\n // then. A clear scheduled at that point — urgent or in its own transition —\n // can commit ahead of the suspended tree, so the link flashes idle for a\n // frame while the old page is still showing (TIM-1418). Wrapping the clear\n // in `startTransition` was tried: it only holds when both updates share a\n // lane, and React assigns lanes per event, so the decode-time clear and the\n // click-time `root.render` never do. (Next.js gets the shared lane because\n // it hands React the tree in the click event; timber fetches first.) The\n // commit itself is the only signal that cannot beat the commit, and it is\n // the router's to give: `onCommit` fires exactly once, on the commit or\n // when the navigation is abandoned — superseded or failed — so the link\n // can never be left pending for a commit that will not come.\n //\n // Order is not fixed: a destination that does not suspend commits before\n // decode finishes (streaming reveal), so whichever of settle/commit comes\n // second clears.\n //\n // `clickSeq` guards the same-link double click: the first navigation is\n // superseded (its promise RESOLVES — `runNavigation` swallows AbortErrors —\n // and its `onCommit` fires) while the second is still in flight, and only\n // the newest click for this link may clear its flag. Written on click and\n // nowhere else, so there is no reset for a settle handler to race.\n const [isPending, setIsPending] = useState(false);\n const clickSeq = useRef(0);\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // Read via getCurrentSearch() rather than a hook, to avoid an\n // unconditional hook call for a prop most links don't pass. On the\n // client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's raw query string.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // Event callbacks may update the URL, so resolve preserved params at use time.\n const resolveEventUrl = () =>\n new URL(\n preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref,\n window.location.href\n );\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Native anchor scrolling is not an SPA navigation. Decide before\n // onNavigate can cancel it, just as we do before hover prefetching.\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n // Keep the post-callback read: onNavigate may change the current URL\n // and therefore the search params to preserve or fragment locality.\n const resolved = resolveEventUrl();\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;\n\n const seq = ++clickSeq.current;\n let settled = false;\n let committed = false;\n const clear = () => {\n if (clickSeq.current === seq) setIsPending(false);\n };\n const onCommit = () => {\n committed = true;\n if (settled) clear();\n };\n const settle = () => {\n settled = true;\n if (committed) clear();\n };\n setIsPending(true);\n const navigation = router.navigate(absoluteHref, {\n scroll: shouldScroll,\n replace,\n transitionTypes,\n onCommit,\n });\n navigation.then(settle, (error: unknown) => {\n clear();\n // Rethrow, so a navigation error that the router did not already\n // recover from surfaces as an unhandled rejection — the same\n // regime as `useRouter().push()`'s `void router.navigate(...)`.\n //\n // It used to reach the nearest error boundary instead: React\n // re-throws a rejected async action during render (measured on\n // 19.2.7). That replaced the departing page with the app's error\n // UI, which contradicts what every other failure path here\n // promises — a navigation that fails leaves the user on the page\n // they were already looking at (TIM-1306). Recoverable failures\n // never get here anyway; `runNavigation` swallows AbortErrors and\n // `recoverFromNavigationError` turns a failed fetch into a full\n // document load.\n throw error;\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n if (isNativeFragmentLink(event.currentTarget)) return;\n const resolved = resolveEventUrl();\n router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (pathname, search) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.ts';\nimport { validateNavigationHref } from '../shared/href-validation.ts';\n\n/** Options for `push` and `replace`. */\nexport interface NavigateOptions {\n /** Set to false to keep the scroll position instead of scrolling to top. */\n scroll?: boolean;\n /**\n * View transition types to add to this navigation, beside the router's own\n * `navigation-forward`. See `NavigationOptions.transitionTypes` — they must\n * go here rather than in a `startTransition` around the call.\n */\n transitionTypes?: readonly string[];\n}\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: NavigateOptions): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: NavigateOptions): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n transitionTypes: options?.transitionTypes,\n });\n },\n replace(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n replace: true,\n transitionTypes: options?.transitionTypes,\n });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * One unconditional read of NavigationContext, on every side (TIM-1425):\n *\n * - In the browser, the provider wraps the RSC payload in the router's `renderTree`, so\n * the pathname updates in the same render pass as the new tree.\n * - During SSR, the wrapper chain mounts the same provider with the request's\n * pathname (TIM-1424), so this is the identical code path — no ALS read,\n * no fallback tiers.\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts, which has a throwing stub\n * (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error — loud, not guessed-at from window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { useNavigationContext } from './navigation-context.ts';\n\n/**\n * Read the current URL pathname.\n *\n * Throws when no NavigationProvider is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n */\nexport function usePathname(): string {\n const nav = useNavigationContext();\n if (nav === null) {\n throw new Error(\n '[timber] usePathname() was called outside the timber app tree ' +\n '(no NavigationProvider found). In tests, render the component ' +\n 'inside the timber providers.'\n );\n }\n return nav.pathname;\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, a single\n * navigate event listener replaces the popstate handler and routes plain\n * `<a>` clicks through the router:\n * - Traversals are intercepted, with the event's AbortSignal linked to the\n * router's fetch\n * - Cross-document push/replace navigations are cancelled and re-run through\n * the router, so the URL commits with the tree (never at intercept time)\n * - Per-entry scroll state via NavigationHistoryEntry.getState()\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.tsx';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { TraverseDirection } from './router-types.ts';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Traverse Direction ──────────────────────────────────────────\n\n/**\n * Which way a traversal goes, as the view transition type the router adds\n * for it (design/37-navigation-api.md §\"Transition types\").\n *\n * The Navigation API numbers the session's entries, so a traversal to a\n * lower index is back and to a higher one is forward — including a jump of\n * several entries (`history.go(-3)`). An index of -1 means the entry is not\n * in this document's list, and then the direction is unknown.\n */\nexport function traverseDirection(\n destinationIndex: number,\n currentIndex: number | undefined\n): TraverseDirection {\n if (currentIndex === undefined || currentIndex < 0 || destinationIndex < 0) {\n return 'navigation-traverse';\n }\n if (destinationIndex < currentIndex) return 'navigation-back';\n if (destinationIndex > currentIndex) return 'navigation-forward';\n return 'navigation-traverse';\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * Push/replace navigations are handed to the router, which commits the URL\n * with the tree. Traversals are intercepted and replayed or fetched.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a cross-document push/replace navigation the router did not start\n * itself: a plain `<a>`, `navigation.navigate()`, or `location.assign()`.\n * The handler has already cancelled the browser's navigation, so the\n * address bar has not moved; the router moves it when the destination\n * commits, as it does for its own navigations.\n */\n onExternalNavigate: (url: string, options: { replace: boolean }) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch. `direction` is\n * the view transition type the render adds — see `traverseDirection`.\n */\n onTraverse: (\n url: string,\n scrollY: number,\n signal: AbortSignal,\n direction: TraverseDirection\n ) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi.\n */\nexport interface NavigationApiController {\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * The address bar moves only when the destination's tree commits\n * (design/19-client-navigation.md §\"prepareNavigation\"). `event.intercept()`\n * would commit the URL as soon as it is called, a full round trip before the\n * page that belongs to it, so push/replace navigations are never\n * intercepted: the router's own navigations never reach here as\n * cross-document events (Link cancels the click and the router commits with\n * `pushState`), and the rest are cancelled and re-run through the router.\n * Only traversals — which the browser has already moved the URL for in every\n * browser — and shallow updates are intercepted.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + stripRscCacheKey(destUrl.search);\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n // Read before intercept(): the current entry is still the one the\n // user is leaving.\n const direction = traverseDirection(event.destination.index, nav.currentEntry?.index);\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal, direction);\n },\n });\n return;\n }\n\n // A same-document push/replace is `history.pushState()` /\n // `replaceState()`: the router's own commit, or app code (nuqs,\n // replaceUrl, a third-party library). The URL is already the\n // destination and the History API patch in router-init syncs the search\n // params, exactly as in browsers without the Navigation API.\n if (event.destination.sameDocument) return;\n\n // A cross-document push/replace the router did not start: a plain\n // `<a>`, `navigation.navigate()`, `location.assign()`. Cancel it and\n // re-run it through the router, which commits the URL with the tree.\n // A navigation the browser will not let us cancel stays a document load.\n // Cancelling rejects a `navigation.navigate()` caller's `committed` and\n // `finished` with AbortError and drops its `state`; the router's own API\n // is `useRouter()` (design/19 §\"The address bar moves on commit\").\n if (!event.cancelable) return;\n event.preventDefault();\n void callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n });\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.ts';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.ts';\nimport { usePathname } from './use-pathname.ts';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Forms use React's own `useActionState`: an action built with\n * `createActionClient` already has the `(prevState, payload)` signature it\n * calls, and the result is typed from it. `parseFormErrors` reads the errors\n * out of that result.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useTransition } from 'react';\nimport type {\n ActionFn,\n ActionResult,\n InputHint,\n ValidationErrors,\n} from '../server/action-client.ts';\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve, reject) => {\n startTransition(async () => {\n // A raw server function that throws rejects (TIM-1570). The caller\n // awaiting `execute` gets that rejection; without the catch the\n // promise would never settle.\n try {\n resolve(\n await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n )\n );\n } catch (error) {\n reject(error);\n }\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** What `parseFormErrors` reads out of an action result. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Read the errors out of an action result — the state `useActionState`\n * returns. `_root` validation errors are form-level; every other key is a\n * field.\n *\n * @example\n * ```tsx\n * const [result, action, isPending] = useActionState(createTodo, null)\n * const errors = parseFormErrors(result)\n * errors.getFieldError('title') // first message for the field, or null\n * ```\n */\nexport function parseFormErrors<TData>(\n result: ActionResult<TData> | null | undefined\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors: ValidationErrors | undefined = result.validationErrors;\n const serverError = result.serverError;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n","/**\n * useFormField — state for a form field that needs JavaScript (a combobox, a\n * chip list, a row list), made to behave like a native uncontrolled input.\n *\n * React resets a form after its action settles, so a native field with a\n * `defaultValue` lands on whatever default the page now renders: the action's\n * `submittedValues`, or fresh page data after a redirect. State held in\n * `useState` misses that reset and keeps showing the pre-submit edit. This\n * hook shows `defaultValue` until the user edits, and drops the edit when the\n * form resets, so it lands where the native fields do.\n *\n * See design/08-forms-and-actions.md §\"The Form Model\".\n */\n\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type Dispatch,\n type RefCallback,\n type SetStateAction,\n} from 'react';\n\n/**\n * State for a field that follows its form's reset.\n *\n * Returns `[value, setValue, ref]`. Attach `ref` to any element inside the\n * `<form>`, and render the value into the form data yourself (usually a\n * hidden input). `value` is `defaultValue` until `setValue` is called, and is\n * `defaultValue` again after the form resets, unless the form cancels its\n * `reset` event. An edit dispatches a bubbling `change` event from the `ref`\n * element after it commits, so a native `change` listener on the form sees it\n * as it sees a native input's (React's synthetic `onChange` reports only\n * inputs, selects and textareas). A reset and a new default fire none, as\n * they fire none for a native input.\n *\n * @example\n * ```tsx\n * // draft: the form's defaults, read from result?.submittedValues ?? pageData\n * const [tags, setTags, ref] = useFormField(draft.tags);\n * <div ref={ref}>\n * <input type=\"hidden\" name=\"tags\" value={tags.join(',')} />\n * <TagPicker value={tags} onChange={setTags} />\n * </div>\n * ```\n */\nexport function useFormField<T>(\n defaultValue: T\n): [value: T, setValue: Dispatch<SetStateAction<T>>, ref: RefCallback<HTMLElement>] {\n // `null` means \"not edited\": the field shows its default. A box, so an edit\n // back to a value equal to the default still counts as an edit.\n const [edit, setEdit] = useState<{ value: T } | null>(null);\n const value = edit ? edit.value : defaultValue;\n\n // The latest default, for an updater called from an old closure. Written\n // after commit, never during render.\n const defaultRef = useRef(defaultValue);\n useLayoutEffect(() => {\n defaultRef.current = defaultValue;\n });\n\n // Resets the form has fired whose outcome this field has not applied yet.\n // A reset's outcome is known only once its dispatch is over: our listener\n // runs on the form before React's delegated `onReset`, which may cancel\n // it. `takeResets` applies every reset whose dispatch has ended, in order:\n // if any of them was not cancelled, the field was reset. Both the deferred\n // clear and `setValue` call it, so writes follow a native input's order:\n // - after `form.reset()` returns, the reset has happened and a write lands\n // on top of it (the write takes the reset, and the clear finds nothing);\n // - during the reset's dispatch (an `onReset` handler), the reset is still\n // pending: the write applies to the current value, and the clear then\n // erases it unless the reset is cancelled, as a reset overwrites a\n // native input written in its handler.\n // A list, not one slot: `reset(); reset()` where only the second is\n // cancelled still resets, as it does the native fields.\n const pendingResets = useRef<Event[]>([]);\n const takeResets = useCallback((): boolean => {\n // eventPhase is NONE once dispatch is over.\n const over = pendingResets.current.filter((e) => e.eventPhase === Event.NONE);\n if (over.length === 0) return false;\n pendingResets.current = pendingResets.current.filter((e) => !over.includes(e));\n return over.some((e) => !e.defaultPrevented);\n }, []);\n\n const setValue = useCallback<Dispatch<SetStateAction<T>>>(\n (next) => {\n const wasReset = takeResets();\n setEdit((latest) => {\n const current = wasReset ? null : latest;\n const base = current ? current.value : defaultRef.current;\n const value = next instanceof Function ? next(base) : next;\n // Unchanged: keep the box, so nothing re-renders and no `change`\n // fires, as a native input fires none for a value it already has.\n return Object.is(value, base) ? current : { value };\n });\n },\n [takeResets]\n );\n\n const element = useRef<HTMLElement | null>(null);\n const ref = useCallback<RefCallback<HTMLElement>>(\n (el) => {\n element.current = el;\n const form = el?.closest('form');\n if (!form) return;\n const onReset = (event: Event) => {\n pendingResets.current.push(event);\n // Dispatch is over by the time a microtask runs.\n queueMicrotask(() => {\n if (takeResets()) setEdit(null);\n });\n };\n form.addEventListener('reset', onReset);\n return () => {\n element.current = null;\n pendingResets.current = [];\n form.removeEventListener('reset', onReset);\n };\n },\n [takeResets]\n );\n\n // Every `setValue` stores a new box, and nothing else does except a reset\n // (which stores null), so a new non-null box is exactly \"the user edited\".\n useEffect(() => {\n if (edit) element.current?.dispatchEvent(new Event('change', { bubbles: true }));\n }, [edit]);\n\n return [value, setValue, ref];\n}\n","/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { toNullProtoRecord, type CoercedParams } from '../shared/param-value.ts';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call throws the outside-the-timber-app-tree error\n * even though the provider is mounted (the module-snapshot fallback it once\n * silently landed on was deleted in TIM-1425).\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from every entry a Server Component imports, so the named form crashed\n * those entries outright (codex, PR #992; originally reproduced against\n * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —\n * the hazard is unchanged for the entries that remain).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null only when no provider\n * is above the caller — a component rendered outside a timber route. During\n * SSR the wrapper chain mounts `PayloadRoot` too (TIM-1424), so both sides\n * resolve through this context.\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: CoercedParams;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * This used to also write a module-level snapshot during render, as the\n * fallback for `useSegmentParams()` called outside a component. That tier is\n * gone (TIM-1425) — the provider is unconditional on every render path,\n * browser and SSR alike, so the hook reads context or throws. Removing the\n * write also removes render-phase shared mutation from the SSR environment,\n * where concurrent requests rendered through this component.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * One unconditional read of ParamsContext, on every side (TIM-1425):\n *\n * - In the browser, `PayloadRoot` publishes the payload's params above the\n * merge point on every render path. Params update atomically with the RSC\n * tree — no timing gap (TIM-1294, TIM-1297).\n * - During SSR, the wrapper chain mounts the same `PayloadRoot`, fed the\n * `params` half of `splitPayloadRoot(root)` — the identical derivation\n * the browser performs at hydration (TIM-1424).\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error.\n *\n * The module-level subscribe/notify machinery and the `currentParams`\n * snapshot that used to back a fourth fallback tier are gone (TIM-1425):\n * the provider is unconditional on every render path, so nothing read them.\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { Routes } from '../index.ts';\nimport { resolveSegmentParams } from '../shared/slot-params.ts';\nimport { useParamsContext } from './params-context.ts';\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * Throws when no `PayloadRoot` is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams {\n const paramsContext = useParamsContext();\n if (paramsContext === null) {\n throw new Error(\n '[timber] useSegmentParams() was called outside the timber app tree ' +\n '(no params provider found). In tests, render the component inside ' +\n 'the timber providers.'\n );\n }\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n}\n"],"mappings":"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"}
|