@kanzo-tech/navigation 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @kanzo-tech/navigation
2
+
3
+ An unsaved-changes guard: one hook, `useBlocker`, in TanStack Router's shape, over the Navigation
4
+ API and `beforeunload` — and a `/next` door for the two App Router navigations the browser cannot
5
+ see in time.
6
+
7
+ ```sh
8
+ pnpm add @kanzo-tech/navigation
9
+ ```
10
+
11
+ ```tsx
12
+ import { useBlocker } from "@kanzo-tech/navigation";
13
+ import { Link, useRouter } from "@kanzo-tech/navigation/next";
14
+
15
+ const blocker = useBlocker({ shouldBlockFn: () => true, disabled: !dirty, withResolver: true });
16
+ // blocker.status === "blocked" → show your AlertDialog; blocker.proceed() leaves, blocker.reset() stays.
17
+ ```
18
+
19
+ ## What it is not
20
+
21
+ **There is no dialog and no copy.** `status`, `proceed` and `reset` drive the product's own
22
+ `AlertDialog`, in the product's own words.
23
+
24
+ **Nothing is patched.** No `history.pushState`, no router internals, no document-level click
25
+ capture. Every navigation is either cancelled through a public API or documented as not covered —
26
+ the table is at <https://kanzo-tech.github.io/ui/docs/navigation-guard>.
27
+
28
+ ## Why a package, and not `@kanzo-tech/ui`
29
+
30
+ `/docs/philosophy` puts *any router integration* in the products, and nothing here draws. It
31
+ depends on nothing of `ui`'s.
32
+
33
+ ## Close the door behind you
34
+
35
+ A raw `next/link` or `useRouter` from `next/navigation` walks past the guard. Forbid both:
36
+
37
+ ```js
38
+ "no-restricted-imports": ["error", {
39
+ paths: [
40
+ { name: "next/link", message: "Use Link from @kanzo-tech/navigation/next." },
41
+ { name: "next/navigation", importNames: ["useRouter"], message: "Use useRouter from @kanzo-tech/navigation/next." },
42
+ ],
43
+ }],
44
+ ```
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@kanzo-tech/navigation` — keeping somebody on a page they have not finished with.
3
+ *
4
+ * One hook, `useBlocker`, in TanStack Router's shape. This door is the browser half and knows no
5
+ * router: back, forward and `history.go` are cancelled through the Navigation API's `navigate`
6
+ * event before they commit, a plain `<a>` or `location.assign` the same way, and reload, close and
7
+ * the URL bar through `beforeunload`. A router's own client navigations are its adapter's —
8
+ * `@kanzo-tech/navigation/next` for the App Router.
9
+ *
10
+ * There is no dialog here and no copy. `status`, `proceed` and `reset` drive the product's own
11
+ * `AlertDialog`, in the product's words.
12
+ *
13
+ * Nothing in this package patches `history`, reads a router's internals, or listens for clicks on
14
+ * the document. `/docs/design/navigation` has each of those and why.
15
+ */
16
+ export { useBlocker } from './use-blocker';
17
+ export type { BlockerResolver, HistoryAction, ShouldBlockFn, ShouldBlockFnArgs, ShouldBlockFnLocation, UseBlockerOpts, } from './types';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EACV,eAAe,EACf,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,GACf,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { useBlocker as r } from "./use-blocker.js";
2
+ export {
3
+ r as useBlocker
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,16 @@
1
+ import { default as NextLink } from 'next/link';
2
+ import { ComponentProps } from 'react';
3
+ type LinkProps = ComponentProps<typeof NextLink>;
4
+ /**
5
+ * `next/link`, with `onNavigate` consulting the blockers.
6
+ *
7
+ * `onNavigate` is the only cancellable moment Next offers a link: it runs after `Link` has
8
+ * prevented the click and before it dispatches. A blocker that answers `false` synchronously costs
9
+ * nothing — the link navigates as it would have. Any other answer cancels now, and a `proceed`
10
+ * re-issues the navigation through `router.push` or `router.replace` with the same `scroll`. The
11
+ * re-issued one does not carry `useLinkStatus`'s pending state; that is the price of a decision
12
+ * that could not be made synchronously.
13
+ */
14
+ export declare function Link(props: LinkProps): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=next-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-link.d.ts","sourceRoot":"","sources":["../src/next-link.tsx"],"names":[],"mappings":"AAEA,OAAO,QAAQ,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAU,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAGpD,KAAK,SAAS,GAAG,cAAc,CAAC,OAAO,QAAQ,CAAC,CAAC;AAyBjD;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,+BA0CpC"}
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as v } from "react/jsx-runtime";
3
+ import d from "next/link";
4
+ import { useRouter as g } from "next/navigation";
5
+ import { useRef as y } from "react";
6
+ import { evaluate as $, locationOf as m } from "./registry.js";
7
+ function k(n) {
8
+ if (typeof n == "string") return n;
9
+ const { pathname: u, query: o, search: s, hash: e } = n;
10
+ let t = s ?? "";
11
+ if (!t && o) {
12
+ const a = new URLSearchParams();
13
+ if (typeof o == "string") t = o;
14
+ else {
15
+ for (const [c, r] of Object.entries(o))
16
+ for (const i of Array.isArray(r) ? r : [r])
17
+ i != null && a.append(c, String(i));
18
+ t = a.toString();
19
+ }
20
+ }
21
+ t && !t.startsWith("?") && (t = `?${t}`);
22
+ const f = e ? e.startsWith("#") ? e : `#${e}` : "";
23
+ return `${u ?? ""}${t}${f}`;
24
+ }
25
+ function D(n) {
26
+ const { href: u, as: o, replace: s, scroll: e, onClick: t, onNavigate: f } = n, a = g(), c = y(void 0);
27
+ return /* @__PURE__ */ v(
28
+ d,
29
+ {
30
+ ...n,
31
+ onClick: (r) => {
32
+ c.current = r.currentTarget.href, t == null || t(r);
33
+ },
34
+ onNavigate: (r) => {
35
+ let i = !1;
36
+ if (f == null || f({
37
+ preventDefault: () => {
38
+ i = !0, r.preventDefault();
39
+ }
40
+ }), i) return;
41
+ const l = k(o ?? u), p = $({
42
+ current: m(location.href),
43
+ next: m(c.current ?? l, location.href),
44
+ action: s ? "REPLACE" : "PUSH"
45
+ });
46
+ p !== !1 && (r.preventDefault(), p !== !0 && p.then((h) => {
47
+ h || (s ? a.replace(l, { scroll: e }) : a.push(l, { scroll: e }));
48
+ }));
49
+ }
50
+ }
51
+ );
52
+ }
53
+ export {
54
+ D as Link
55
+ };
56
+ //# sourceMappingURL=next-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-link.js","sources":["../src/next-link.tsx"],"sourcesContent":["\"use client\";\n\nimport NextLink from \"next/link\";\nimport { useRouter as useNextRouter } from \"next/navigation\";\nimport { useRef, type ComponentProps } from \"react\";\nimport { evaluate, locationOf } from \"./registry\";\n\ntype LinkProps = ComponentProps<typeof NextLink>;\ntype Href = LinkProps[\"href\"];\n\n/** The URL a `Link` goes to, spelled the way `router.push` takes it. */\nfunction hrefOf(href: Href): string {\n if (typeof href === \"string\") return href;\n const { pathname, query, search, hash } = href;\n let tail = search ?? \"\";\n if (!tail && query) {\n const params = new URLSearchParams();\n if (typeof query === \"string\") tail = query;\n else {\n for (const [key, value] of Object.entries(query)) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (item !== undefined && item !== null) params.append(key, String(item));\n }\n }\n tail = params.toString();\n }\n }\n if (tail && !tail.startsWith(\"?\")) tail = `?${tail}`;\n const fragment = hash ? (hash.startsWith(\"#\") ? hash : `#${hash}`) : \"\";\n return `${pathname ?? \"\"}${tail}${fragment}`;\n}\n\n/**\n * `next/link`, with `onNavigate` consulting the blockers.\n *\n * `onNavigate` is the only cancellable moment Next offers a link: it runs after `Link` has\n * prevented the click and before it dispatches. A blocker that answers `false` synchronously costs\n * nothing — the link navigates as it would have. Any other answer cancels now, and a `proceed`\n * re-issues the navigation through `router.push` or `router.replace` with the same `scroll`. The\n * re-issued one does not carry `useLinkStatus`'s pending state; that is the price of a decision\n * that could not be made synchronously.\n */\nexport function Link(props: LinkProps) {\n const { href, as, replace, scroll, onClick, onNavigate } = props;\n const router = useNextRouter();\n // The anchor's own `href` is the destination as the browser resolved it — base path, relative\n // segments and all — which is what `shouldBlockFn` is owed. `onClick` runs just before\n // `onNavigate`, on the same click.\n const clicked = useRef<string | undefined>(undefined);\n\n return (\n <NextLink\n {...props}\n onClick={(event) => {\n clicked.current = event.currentTarget.href;\n onClick?.(event);\n }}\n onNavigate={(event) => {\n let prevented = false;\n onNavigate?.({\n preventDefault: () => {\n prevented = true;\n event.preventDefault();\n },\n });\n if (prevented) return;\n\n const target = hrefOf(as ?? href);\n const verdict = evaluate({\n current: locationOf(location.href),\n next: locationOf(clicked.current ?? target, location.href),\n action: replace ? \"REPLACE\" : \"PUSH\",\n });\n if (verdict === false) return;\n event.preventDefault();\n if (verdict === true) return;\n void verdict.then((block) => {\n if (block) return;\n if (replace) router.replace(target, { scroll });\n else router.push(target, { scroll });\n });\n }}\n />\n );\n}\n"],"names":[],"mappings":";;;;;;AAWA;AACE;AACA;AACA;AACA;AACE;AACA;AAAsC;AAEpC;AACE;AACE;AAGJ;AAAc;AAChB;AAEF;AACA;AACA;AACF;AAYO;AACL;AAOA;AACE;AAAC;AAAA;AACK;AAEF;AACU;AACZ;AAEE;AAOA;AANa;AAET;AACM;AACR;AAIF;AACyB;AACU;AACwB;AAC3B;AAEhC;AAIE;AAEmC;AACpC;AACH;AAAA;AAGN;;;;"}
@@ -0,0 +1,12 @@
1
+ import { useRouter as useNextRouter } from 'next/navigation';
2
+ type Router = ReturnType<typeof useNextRouter>;
3
+ /**
4
+ * `next/navigation`'s `useRouter`, with `push` and `replace` consulting the blockers first.
5
+ *
6
+ * It wraps the instance Next hands every component, and nothing below it: `back`, `forward`,
7
+ * `refresh` and `prefetch` are Next's own. `back` and `forward` are traversals, which the root's
8
+ * `navigate` listener already sees before they commit; `refresh` does not leave the page.
9
+ */
10
+ export declare function useRouter(): Router;
11
+ export {};
12
+ //# sourceMappingURL=next-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-router.d.ts","sourceRoot":"","sources":["../src/next-router.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAK7D,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AAE/C;;;;;;GAMG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAyBlC"}
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { useRouter as s } from "next/navigation";
3
+ import { useMemo as f } from "react";
4
+ import { evaluate as l, locationOf as c } from "./registry.js";
5
+ function R() {
6
+ const e = s();
7
+ return f(() => {
8
+ const o = (i, u) => (r, n) => {
9
+ const t = l({
10
+ current: c(location.href),
11
+ next: c(r, location.href),
12
+ action: i
13
+ });
14
+ if (t === !1) return u(r, n);
15
+ t !== !0 && t.then((a) => {
16
+ a || u(r, n);
17
+ });
18
+ };
19
+ return {
20
+ ...e,
21
+ push: o("PUSH", e.push),
22
+ replace: o("REPLACE", e.replace)
23
+ };
24
+ }, [e]);
25
+ }
26
+ export {
27
+ R as useRouter
28
+ };
29
+ //# sourceMappingURL=next-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-router.js","sources":["../src/next-router.ts"],"sourcesContent":["\"use client\";\n\nimport { useRouter as useNextRouter } from \"next/navigation\";\nimport { useMemo } from \"react\";\nimport { evaluate, locationOf } from \"./registry\";\nimport type { HistoryAction } from \"./types\";\n\ntype Router = ReturnType<typeof useNextRouter>;\n\n/**\n * `next/navigation`'s `useRouter`, with `push` and `replace` consulting the blockers first.\n *\n * It wraps the instance Next hands every component, and nothing below it: `back`, `forward`,\n * `refresh` and `prefetch` are Next's own. `back` and `forward` are traversals, which the root's\n * `navigate` listener already sees before they commit; `refresh` does not leave the page.\n */\nexport function useRouter(): Router {\n const router = useNextRouter();\n\n return useMemo(() => {\n const guarded =\n (action: HistoryAction, go: Router[\"push\"]): Router[\"push\"] =>\n (href, options) => {\n const verdict = evaluate({\n current: locationOf(location.href),\n next: locationOf(href, location.href),\n action,\n });\n if (verdict === false) return go(href, options);\n if (verdict === true) return;\n void verdict.then((block) => {\n if (!block) go(href, options);\n });\n };\n\n return {\n ...router,\n push: guarded(\"PUSH\", router.push),\n replace: guarded(\"REPLACE\", router.replace),\n };\n }, [router]);\n}\n"],"names":[],"mappings":";;;;AAgBO;AACL;AAEA;AACE;AAGI;AAAyB;AACU;AACG;AACpC;AAEF;AACA;AAEE;AAA4B;AAC7B;AAGL;AAAO;AACF;AAC8B;AACS;AAAA;AAGhD;;;;"}
package/dist/next.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@kanzo-tech/navigation/next` — the two App Router navigations the root cannot see.
3
+ *
4
+ * Next writes the URL of a client navigation *after* the new route has rendered, so by the time the
5
+ * browser reports it there is nothing left to cancel. What Next does offer is public and early:
6
+ * `Link`'s `onNavigate`, and the router a component asks for. This door is those two, wired to the
7
+ * same blockers `useBlocker` registers.
8
+ *
9
+ * ```ts
10
+ * import { Link, useRouter } from "@kanzo-tech/navigation/next";
11
+ * ```
12
+ *
13
+ * A raw `next/link` or `useRouter` from `next/navigation` walks past both, and nothing at runtime
14
+ * can notice. The lint rule that closes that is on `/docs/navigation-guard/next`.
15
+ */
16
+ export { Link } from './next-link';
17
+ export { useRouter } from './next-router';
18
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
package/dist/next.js ADDED
@@ -0,0 +1,7 @@
1
+ import { Link as e } from "./next-link.js";
2
+ import { useRouter as f } from "./next-router.js";
3
+ export {
4
+ e as Link,
5
+ f as useRouter
6
+ };
7
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,27 @@
1
+ import { Blocker } from './registry';
2
+ /**
3
+ * The browser half: the Navigation API's `navigate` event and `beforeunload`. Nothing here knows a
4
+ * router, and nothing here touches `history` — every navigation is either cancelled through a
5
+ * public event or left alone.
6
+ *
7
+ * The listeners exist only while a blocker does. MDN: a `beforeunload` listener costs a page its
8
+ * back/forward cache in Firefox, so a page with nothing to protect must carry none.
9
+ */
10
+ /** The slice of the Navigation API this reads. TypeScript's DOM lib does not declare it yet. */
11
+ interface HistoryEntry {
12
+ key: string;
13
+ index: number;
14
+ url: string | null;
15
+ }
16
+ export interface Navigation extends EventTarget {
17
+ currentEntry: HistoryEntry | null;
18
+ navigate(url: string, options?: {
19
+ history?: "push" | "replace";
20
+ state?: unknown;
21
+ }): unknown;
22
+ traverseTo(key: string): unknown;
23
+ }
24
+ /** Adds a blocker at the end of the order and returns what removes it. */
25
+ export declare function register(blocker: Blocker): () => void;
26
+ export {};
27
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0C,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAGlF;;;;;;;GAOG;AAEH,gGAAgG;AAChG,UAAU,YAAY;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAeD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC;IAC5F,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAgFD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,IAAI,CASrD"}
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { blockers as r, bypass as t, evaluate as d, locationOf as u } from "./registry.js";
3
+ const o = () => window.navigation;
4
+ function c(e, n) {
5
+ var a;
6
+ if (e.navigationType === "push") return "PUSH";
7
+ if (e.navigationType === "replace") return "REPLACE";
8
+ const i = e.destination.index - (((a = n.currentEntry) == null ? void 0 : a.index) ?? -2);
9
+ return i === -1 ? "BACK" : i === 1 ? "FORWARD" : "GO";
10
+ }
11
+ function v(e, n) {
12
+ if (t.navigate = !0, e.destination.sameDocument || (t.beforeunload = !0), e.navigationType === "traverse") {
13
+ n.traverseTo(e.destination.key);
14
+ return;
15
+ }
16
+ n.navigate(e.destination.url, {
17
+ history: e.navigationType === "replace" ? "replace" : "push",
18
+ state: e.destination.getState()
19
+ });
20
+ }
21
+ function f(e) {
22
+ const n = e, i = o();
23
+ if (!i) return;
24
+ if (t.navigate) {
25
+ t.navigate = !1;
26
+ return;
27
+ }
28
+ if (n.navigationType === "reload" || n.navigationType !== "traverse" && n.destination.sameDocument || n.formData !== null || n.downloadRequest !== null || !n.cancelable) return;
29
+ const a = d({
30
+ current: u(location.href),
31
+ next: u(n.destination.url),
32
+ action: c(n, i)
33
+ });
34
+ a !== !1 && (n.preventDefault(), a !== !0 && a.then((l) => {
35
+ l || v(n, i);
36
+ }));
37
+ }
38
+ function s(e) {
39
+ if (t.beforeunload) {
40
+ t.beforeunload = !1;
41
+ return;
42
+ }
43
+ r.some((n) => n.enableBeforeUnload()) && (e.preventDefault(), e.returnValue = "");
44
+ }
45
+ function g() {
46
+ var e;
47
+ window.addEventListener("beforeunload", s), (e = o()) == null || e.addEventListener("navigate", f);
48
+ }
49
+ function p() {
50
+ var e;
51
+ window.removeEventListener("beforeunload", s), (e = o()) == null || e.removeEventListener("navigate", f), t.navigate = !1, t.beforeunload = !1;
52
+ }
53
+ function m(e) {
54
+ return r.length === 0 && g(), r.push(e), () => {
55
+ const n = r.indexOf(e);
56
+ n !== -1 && (r.splice(n, 1), r.length === 0 && p());
57
+ };
58
+ }
59
+ export {
60
+ m as register
61
+ };
62
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sources":["../src/platform.ts"],"sourcesContent":["\"use client\";\n\nimport { blockers, bypass, evaluate, locationOf, type Blocker } from \"./registry\";\nimport type { HistoryAction } from \"./types\";\n\n/**\n * The browser half: the Navigation API's `navigate` event and `beforeunload`. Nothing here knows a\n * router, and nothing here touches `history` — every navigation is either cancelled through a\n * public event or left alone.\n *\n * The listeners exist only while a blocker does. MDN: a `beforeunload` listener costs a page its\n * back/forward cache in Firefox, so a page with nothing to protect must carry none.\n */\n\n/** The slice of the Navigation API this reads. TypeScript's DOM lib does not declare it yet. */\ninterface HistoryEntry {\n key: string;\n index: number;\n url: string | null;\n}\n\ninterface NavigateEvent extends Event {\n navigationType: \"push\" | \"replace\" | \"reload\" | \"traverse\";\n destination: {\n url: string;\n key: string;\n index: number;\n sameDocument: boolean;\n getState(): unknown;\n };\n formData: FormData | null;\n downloadRequest: string | null;\n}\n\nexport interface Navigation extends EventTarget {\n currentEntry: HistoryEntry | null;\n navigate(url: string, options?: { history?: \"push\" | \"replace\"; state?: unknown }): unknown;\n traverseTo(key: string): unknown;\n}\n\nconst navigationOf = (): Navigation | undefined =>\n (window as unknown as { navigation?: Navigation }).navigation;\n\nfunction actionOf(event: NavigateEvent, navigation: Navigation): HistoryAction {\n if (event.navigationType === \"push\") return \"PUSH\";\n if (event.navigationType === \"replace\") return \"REPLACE\";\n const delta = event.destination.index - (navigation.currentEntry?.index ?? -2);\n return delta === -1 ? \"BACK\" : delta === 1 ? \"FORWARD\" : \"GO\";\n}\n\n/** Replays a navigation `proceed` allowed, past the blockers it was cancelled by. */\nfunction replay(event: NavigateEvent, navigation: Navigation) {\n bypass.navigate = true;\n if (!event.destination.sameDocument) bypass.beforeunload = true;\n if (event.navigationType === \"traverse\") {\n navigation.traverseTo(event.destination.key);\n return;\n }\n navigation.navigate(event.destination.url, {\n history: event.navigationType === \"replace\" ? \"replace\" : \"push\",\n state: event.destination.getState(),\n });\n}\n\nfunction onNavigate(raw: Event) {\n const event = raw as NavigateEvent;\n const navigation = navigationOf();\n if (!navigation) return;\n if (bypass.navigate) {\n bypass.navigate = false;\n return;\n }\n // A reload is `beforeunload`'s. A same-document push or replace is a router writing the URL of a\n // page it has already rendered — too late to cancel — or a hash, which leaves nothing.\n if (event.navigationType === \"reload\") return;\n if (event.navigationType !== \"traverse\" && event.destination.sameDocument) return;\n // A POST cannot be replayed through `navigation.navigate`, and a download leaves the page as it\n // was. Both stay with the browser, which asks through `beforeunload` where it applies.\n if (event.formData !== null || event.downloadRequest !== null) return;\n // Browser UI, and a traversal with no history-action activation left: the platform decides.\n if (!event.cancelable) return;\n\n const verdict = evaluate({\n current: locationOf(location.href),\n next: locationOf(event.destination.url),\n action: actionOf(event, navigation),\n });\n if (verdict === false) return;\n event.preventDefault();\n if (verdict === true) return;\n void verdict.then((block) => {\n if (!block) replay(event, navigation);\n });\n}\n\nfunction onBeforeUnload(event: BeforeUnloadEvent) {\n if (bypass.beforeunload) {\n bypass.beforeunload = false;\n return;\n }\n if (!blockers.some((blocker) => blocker.enableBeforeUnload())) return;\n event.preventDefault();\n // Still required by Chromium-based browsers for the dialog to show; the text itself is ignored.\n event.returnValue = \"\";\n}\n\nfunction listen() {\n window.addEventListener(\"beforeunload\", onBeforeUnload);\n navigationOf()?.addEventListener(\"navigate\", onNavigate);\n}\n\nfunction unlisten() {\n window.removeEventListener(\"beforeunload\", onBeforeUnload);\n navigationOf()?.removeEventListener(\"navigate\", onNavigate);\n bypass.navigate = false;\n bypass.beforeunload = false;\n}\n\n/** Adds a blocker at the end of the order and returns what removes it. */\nexport function register(blocker: Blocker): () => void {\n if (blockers.length === 0) listen();\n blockers.push(blocker);\n return () => {\n const at = blockers.indexOf(blocker);\n if (at === -1) return;\n blockers.splice(at, 1);\n if (blockers.length === 0) unlisten();\n };\n}\n"],"names":[],"mappings":";;AAwCA;AAGA;;AACE;AACA;AACA;AACA;AACF;AAGA;AAGE;AACE;AACA;AAAA;AAEF;AAA2C;AACiB;AACjC;AAE7B;AAEA;AACE;AAEA;AACA;AACE;AACA;AAAA;AAUF;AAEA;AAAyB;AACU;AACK;AACJ;AAEpC;AAIE;AAAoC;AAExC;AAEA;AACE;AACE;AACA;AAAA;AAEF;AAIF;AAEA;;AACE;AAEF;AAEA;;AACE;AAIF;AAGO;AACL;AAGE;AACA;AAE2B;AAE/B;;;;"}
@@ -0,0 +1,34 @@
1
+ import { ShouldBlockFnArgs, ShouldBlockFnLocation } from './types';
2
+ /**
3
+ * The blockers on the page, and the one evaluation every door shares.
4
+ *
5
+ * Module state on purpose: the root's listeners and `/next`'s `Link` and `useRouter` must consult
6
+ * the same list, and the build writes one file per module so both doors import this one rather than
7
+ * each inlining a copy. No DOM here — `platform.ts` is the half that touches `window`.
8
+ */
9
+ export interface Blocker {
10
+ /** `true` blocks, `false` lets it through; a promise is a decision still being made. */
11
+ fn: (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
12
+ enableBeforeUnload: () => boolean;
13
+ }
14
+ export declare const blockers: Blocker[];
15
+ /**
16
+ * One-shot passes for a navigation this package is replaying after `proceed`, so the replay is not
17
+ * blocked a second time. TanStack's `ignoreNextBeforeUnload` and `skipBlockerNextPop`, in the two
18
+ * places a replay reaches here.
19
+ */
20
+ export declare const bypass: {
21
+ navigate: boolean;
22
+ beforeunload: boolean;
23
+ };
24
+ /**
25
+ * Blockers in registration order, and the first that says `true` blocks.
26
+ *
27
+ * Synchronous for as long as every blocker answers synchronously, which is what lets a clean form
28
+ * leave `Link` and `navigate` completely alone. A promise anywhere turns the rest of the walk into
29
+ * one: the caller then has to cancel now and replay later, because both hooks it sits on demand a
30
+ * synchronous `preventDefault()`.
31
+ */
32
+ export declare function evaluate(args: ShouldBlockFnArgs): boolean | Promise<boolean>;
33
+ export declare function locationOf(url: string | URL, base?: string): ShouldBlockFnLocation;
34
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAExE;;;;;;GAMG;AAEH,MAAM,WAAW,OAAO;IACtB,wFAAwF;IACxF,EAAE,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,kBAAkB,EAAE,MAAM,OAAO,CAAC;CACnC;AAED,eAAO,MAAM,QAAQ,EAAE,OAAO,EAAO,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,MAAM;;;CAA2C,CAAC;AAE/D;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAGlF"}
@@ -0,0 +1,25 @@
1
+ const a = [], l = { navigate: !1, beforeunload: !1 };
2
+ function u(n) {
3
+ const e = (t) => {
4
+ const r = a.slice(t);
5
+ for (const [o, s] of r.entries()) {
6
+ const f = s.fn(n);
7
+ if (f === !0) return !0;
8
+ if (f !== !1)
9
+ return Promise.resolve(f).then((c) => c ? !0 : e(t + o + 1));
10
+ }
11
+ return !1;
12
+ };
13
+ return e(0);
14
+ }
15
+ function i(n, e) {
16
+ const { href: t, pathname: r, search: o, hash: s } = new URL(n, e);
17
+ return { href: t, pathname: r, search: o, hash: s };
18
+ }
19
+ export {
20
+ a as blockers,
21
+ l as bypass,
22
+ u as evaluate,
23
+ i as locationOf
24
+ };
25
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sources":["../src/registry.ts"],"sourcesContent":["import type { ShouldBlockFnArgs, ShouldBlockFnLocation } from \"./types\";\n\n/**\n * The blockers on the page, and the one evaluation every door shares.\n *\n * Module state on purpose: the root's listeners and `/next`'s `Link` and `useRouter` must consult\n * the same list, and the build writes one file per module so both doors import this one rather than\n * each inlining a copy. No DOM here — `platform.ts` is the half that touches `window`.\n */\n\nexport interface Blocker {\n /** `true` blocks, `false` lets it through; a promise is a decision still being made. */\n fn: (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;\n enableBeforeUnload: () => boolean;\n}\n\nexport const blockers: Blocker[] = [];\n\n/**\n * One-shot passes for a navigation this package is replaying after `proceed`, so the replay is not\n * blocked a second time. TanStack's `ignoreNextBeforeUnload` and `skipBlockerNextPop`, in the two\n * places a replay reaches here.\n */\nexport const bypass = { navigate: false, beforeunload: false };\n\n/**\n * Blockers in registration order, and the first that says `true` blocks.\n *\n * Synchronous for as long as every blocker answers synchronously, which is what lets a clean form\n * leave `Link` and `navigate` completely alone. A promise anywhere turns the rest of the walk into\n * one: the caller then has to cancel now and replay later, because both hooks it sits on demand a\n * synchronous `preventDefault()`.\n */\nexport function evaluate(args: ShouldBlockFnArgs): boolean | Promise<boolean> {\n const from = (start: number): boolean | Promise<boolean> => {\n const list = blockers.slice(start);\n for (const [offset, blocker] of list.entries()) {\n const verdict = blocker.fn(args);\n if (verdict === true) return true;\n if (verdict !== false) {\n return Promise.resolve(verdict).then((block) => (block ? true : from(start + offset + 1)));\n }\n }\n return false;\n };\n return from(0);\n}\n\nexport function locationOf(url: string | URL, base?: string): ShouldBlockFnLocation {\n const { href, pathname, search, hash } = new URL(url, base);\n return { href, pathname, search, hash };\n}\n"],"names":["blockers","bypass","evaluate","args","from","start","list","offset","blocker","verdict","block","locationOf","url","base","href","pathname","search","hash"],"mappings":"AAgBO,MAAMA,IAAsB,CAAA,GAOtBC,IAAS,EAAE,UAAU,IAAO,cAAc,GAAA;AAUhD,SAASC,EAASC,GAAqD;AAC5E,QAAMC,IAAO,CAACC,MAA8C;AAC1D,UAAMC,IAAON,EAAS,MAAMK,CAAK;AACjC,eAAW,CAACE,GAAQC,CAAO,KAAKF,EAAK,WAAW;AAC9C,YAAMG,IAAUD,EAAQ,GAAGL,CAAI;AAC/B,UAAIM,MAAY,GAAM,QAAO;AAC7B,UAAIA,MAAY;AACd,eAAO,QAAQ,QAAQA,CAAO,EAAE,KAAK,CAACC,MAAWA,IAAQ,KAAON,EAAKC,IAAQE,IAAS,CAAC,CAAE;AAAA,IAE7F;AACA,WAAO;AAAA,EACT;AACA,SAAOH,EAAK,CAAC;AACf;AAEO,SAASO,EAAWC,GAAmBC,GAAsC;AAClF,QAAM,EAAE,MAAAC,GAAM,UAAAC,GAAU,QAAAC,GAAQ,MAAAC,MAAS,IAAI,IAAIL,GAAKC,CAAI;AAC1D,SAAO,EAAE,MAAAC,GAAM,UAAAC,GAAU,QAAAC,GAAQ,MAAAC,EAAA;AACnC;"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * TanStack Router's blocker vocabulary, name for name. The one shape that differs is the location:
3
+ * TanStack hands `{ routeId, fullPath, params }`, and Next matches routes on the server only, so
4
+ * what a client can honestly say about a destination is its URL.
5
+ */
6
+ export type HistoryAction = "PUSH" | "REPLACE" | "BACK" | "FORWARD" | "GO";
7
+ export interface ShouldBlockFnLocation {
8
+ href: string;
9
+ pathname: string;
10
+ search: string;
11
+ hash: string;
12
+ }
13
+ export interface ShouldBlockFnArgs {
14
+ current: ShouldBlockFnLocation;
15
+ next: ShouldBlockFnLocation;
16
+ action: HistoryAction;
17
+ }
18
+ export type ShouldBlockFn = (args: ShouldBlockFnArgs) => boolean | Promise<boolean>;
19
+ export interface UseBlockerOpts {
20
+ shouldBlockFn: ShouldBlockFn;
21
+ /** Ask the browser's own dialog on reload, close and the URL bar. `true` by default. */
22
+ enableBeforeUnload?: boolean | (() => boolean);
23
+ disabled?: boolean;
24
+ withResolver?: boolean;
25
+ }
26
+ export type BlockerResolver = {
27
+ status: "blocked";
28
+ current: ShouldBlockFnLocation;
29
+ next: ShouldBlockFnLocation;
30
+ action: HistoryAction;
31
+ proceed: () => void;
32
+ reset: () => void;
33
+ } | {
34
+ status: "idle";
35
+ current: undefined;
36
+ next: undefined;
37
+ action: undefined;
38
+ proceed: undefined;
39
+ reset: undefined;
40
+ };
41
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,eAAe,GACvB;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,GACD;IACE,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,SAAS,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;CAClB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { BlockerResolver, UseBlockerOpts } from './types';
2
+ /**
3
+ * Blocks leaving the page while `shouldBlockFn` says so. TanStack Router's `useBlocker`, verbatim in
4
+ * its options and its resolver; `/docs/design/navigation` is what differs and why.
5
+ *
6
+ * With `withResolver`, a navigation `shouldBlockFn` blocks is held — `status: "blocked"` — until
7
+ * `proceed` lets it continue or `reset` stays. The dialog that asks is the product's.
8
+ *
9
+ * `enableBeforeUnload` is **not** derived from `shouldBlockFn`: the browser asks synchronously and
10
+ * `shouldBlockFn` may not answer that way. Left at its default, a page carrying this hook is asked
11
+ * about on every reload — which is why `disabled: !dirty` is the idiom, not `shouldBlockFn: () =>
12
+ * dirty` alone.
13
+ */
14
+ export declare function useBlocker(opts: UseBlockerOpts & {
15
+ withResolver: true;
16
+ }): BlockerResolver;
17
+ export declare function useBlocker(opts: UseBlockerOpts & {
18
+ withResolver?: false;
19
+ }): void;
20
+ export declare function useBlocker(opts: UseBlockerOpts): BlockerResolver | void;
21
+ //# sourceMappingURL=use-blocker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-blocker.d.ts","sourceRoot":"","sources":["../src/use-blocker.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAqB,cAAc,EAAE,MAAM,SAAS,CAAC;AAWlF;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,GAAG,eAAe,CAAC;AAC3F,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,YAAY,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAAC;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,IAAI,CAAC"}
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { useState as k, useRef as p, useEffect as b } from "react";
3
+ import { register as x } from "./platform.js";
4
+ const d = {
5
+ status: "idle",
6
+ current: void 0,
7
+ next: void 0,
8
+ action: void 0,
9
+ proceed: void 0,
10
+ reset: void 0
11
+ };
12
+ function h({
13
+ shouldBlockFn: u,
14
+ enableBeforeUnload: c = !0,
15
+ disabled: f = !1,
16
+ withResolver: i = !1
17
+ }) {
18
+ const [v, l] = k(d), o = p({ shouldBlockFn: u, enableBeforeUnload: c });
19
+ return o.current = { shouldBlockFn: u, enableBeforeUnload: c }, b(() => {
20
+ if (f) return;
21
+ let t;
22
+ const a = (r) => {
23
+ t == null || t(!0);
24
+ let e = () => {
25
+ };
26
+ const n = new Promise((s) => e = s);
27
+ return t = e, l({
28
+ status: "blocked",
29
+ ...r,
30
+ proceed: () => e(!1),
31
+ reset: () => e(!0)
32
+ }), n.then((s) => (t === e && (t = void 0, l(d)), s));
33
+ }, m = x({
34
+ fn: (r) => {
35
+ const e = o.current.shouldBlockFn(r);
36
+ return !i || e === !1 ? e : e === !0 ? a(r) : e.then((n) => n ? a(r) : !1);
37
+ },
38
+ enableBeforeUnload: () => {
39
+ const r = o.current.enableBeforeUnload;
40
+ return typeof r == "function" ? r() : r;
41
+ }
42
+ });
43
+ return () => {
44
+ m(), t == null || t(!0);
45
+ };
46
+ }, [f, i]), v;
47
+ }
48
+ export {
49
+ h as useBlocker
50
+ };
51
+ //# sourceMappingURL=use-blocker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-blocker.js","sources":["../src/use-blocker.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { register } from \"./platform\";\nimport type { BlockerResolver, ShouldBlockFnArgs, UseBlockerOpts } from \"./types\";\n\nconst IDLE: BlockerResolver = {\n status: \"idle\",\n current: undefined,\n next: undefined,\n action: undefined,\n proceed: undefined,\n reset: undefined,\n};\n\n/**\n * Blocks leaving the page while `shouldBlockFn` says so. TanStack Router's `useBlocker`, verbatim in\n * its options and its resolver; `/docs/design/navigation` is what differs and why.\n *\n * With `withResolver`, a navigation `shouldBlockFn` blocks is held — `status: \"blocked\"` — until\n * `proceed` lets it continue or `reset` stays. The dialog that asks is the product's.\n *\n * `enableBeforeUnload` is **not** derived from `shouldBlockFn`: the browser asks synchronously and\n * `shouldBlockFn` may not answer that way. Left at its default, a page carrying this hook is asked\n * about on every reload — which is why `disabled: !dirty` is the idiom, not `shouldBlockFn: () =>\n * dirty` alone.\n */\nexport function useBlocker(opts: UseBlockerOpts & { withResolver: true }): BlockerResolver;\nexport function useBlocker(opts: UseBlockerOpts & { withResolver?: false }): void;\nexport function useBlocker(opts: UseBlockerOpts): BlockerResolver | void;\nexport function useBlocker({\n shouldBlockFn,\n enableBeforeUnload = true,\n disabled = false,\n withResolver = false,\n}: UseBlockerOpts): BlockerResolver | void {\n const [resolver, setResolver] = useState<BlockerResolver>(IDLE);\n\n // The latest options, read at navigation time. Registering again whenever an inline function\n // changed identity would move this blocker to the end of the order on every render.\n const latest = useRef({ shouldBlockFn, enableBeforeUnload });\n latest.current = { shouldBlockFn, enableBeforeUnload };\n\n useEffect(() => {\n if (disabled) return;\n let pending: ((block: boolean) => void) | undefined;\n\n const ask = (args: ShouldBlockFnArgs) => {\n // A second navigation while the first is still being asked about: the first one stays.\n pending?.(true);\n let settle: (block: boolean) => void = () => {};\n const asked = new Promise<boolean>((resolve) => (settle = resolve));\n pending = settle;\n setResolver({\n status: \"blocked\",\n ...args,\n proceed: () => settle(false),\n reset: () => settle(true),\n });\n return asked.then((block) => {\n // Only the navigation still being asked about may put the resolver back to idle.\n if (pending === settle) {\n pending = undefined;\n setResolver(IDLE);\n }\n return block;\n });\n };\n\n const unregister = register({\n fn: (args) => {\n const verdict = latest.current.shouldBlockFn(args);\n if (!withResolver || verdict === false) return verdict;\n if (verdict === true) return ask(args);\n return verdict.then((block) => (block ? ask(args) : false));\n },\n enableBeforeUnload: () => {\n const enabled = latest.current.enableBeforeUnload;\n return typeof enabled === \"function\" ? enabled() : enabled;\n },\n });\n\n return () => {\n unregister();\n pending?.(true);\n };\n }, [disabled, withResolver]);\n\n return resolver;\n}\n"],"names":[],"mappings":";;;AAMA;AAA8B;AACpB;AACC;AACH;AACE;AACC;AAEX;AAiBO;AAAoB;AACzB;AACqB;AACV;AAEb;AACE;AAKA;AAGE;AACA;AAEA;AAEE;AACA;AAA6C;AAC7C;AACA;AACY;AACF;AACL;AACwB;AACH;AASzB;AAGyB;AAExB;AACA;AAE0D;AAC5D;AAEE;AACA;AAAmD;AACrD;AAGF;AACE;AACU;AACZ;AAIJ;;;;"}
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@kanzo-tech/navigation",
3
+ "version": "0.11.0",
4
+ "description": "Kanzo's unsaved-changes guard — one useBlocker in TanStack Router's shape over the Navigation API and beforeunload, and a /next door for the App Router's Link and router. Sibling of @kanzo-tech/ui, not part of it: a router integration is product territory by /docs/philosophy, and nothing here draws.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./next": {
20
+ "types": "./dist/next.d.ts",
21
+ "import": "./dist/next.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "//peers": "react is REQUIRED: the root is a hook. next is optional and lives behind `./next`, from 15.3 because that is the release `Link` gained `onNavigate` — the only cancellable moment Next gives a link. A host that is not Next installs this package alone and gets the browser half. Deliberately absent: tailwind-variants and @kanzo-tech/ui, because nothing here draws; the confirm dialog is the product's AlertDialog. That absence is also what keeps this package out of the appearance guards, whose corpus in packages/ui/src/guard-corpus.ts selects on that dependency.",
26
+ "peerDependencies": {
27
+ "next": ">=15.3",
28
+ "react": ">=19"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "next": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/dom": "^10.4.1",
37
+ "@testing-library/react": "^16.3.2",
38
+ "@types/react": "^19.0.0",
39
+ "@types/react-dom": "^19.0.0",
40
+ "jsdom": "^29.1.1",
41
+ "next": "^16.2.7",
42
+ "react": "^19.0.0",
43
+ "react-dom": "^19.0.0",
44
+ "rollup-plugin-preserve-directives": "^0.4.0"
45
+ },
46
+ "//size-limit": "Set just above the first measurement, for the reason auth's note gives: a budget with 70% headroom detects nothing. The root barrel measured 1.06 kB — the hook, the registry and the two listeners — and `./next` 1.04 kB — the two adapters and the registry they share with the root; both budgets are 1.2 kB. `next` is ignored on the second because the consumer installs it and what is measured is our adapter over it. What these numbers watch for is a door reaching through another: the root learning a router, or `./next` pulling the hook and the listeners in behind `Link`. Either is a jump of the size of the other door, and a raise is a decision written here, never a quiet edit.",
47
+ "size-limit": [
48
+ {
49
+ "name": "root barrel (JS)",
50
+ "path": "dist/index.js",
51
+ "limit": "1.2 kB",
52
+ "ignore": [
53
+ "react",
54
+ "react-dom",
55
+ "react/jsx-runtime"
56
+ ]
57
+ },
58
+ {
59
+ "name": "next subpath (JS)",
60
+ "path": "dist/next.js",
61
+ "limit": "1.2 kB",
62
+ "ignore": [
63
+ "react",
64
+ "react-dom",
65
+ "react/jsx-runtime",
66
+ "next",
67
+ "next/link",
68
+ "next/navigation"
69
+ ]
70
+ }
71
+ ],
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/Kanzo-Tech/ui.git",
75
+ "directory": "packages/navigation"
76
+ },
77
+ "homepage": "https://kanzo-tech.github.io/ui",
78
+ "bugs": {
79
+ "url": "https://github.com/Kanzo-Tech/ui/issues"
80
+ },
81
+ "scripts": {
82
+ "build": "vite build",
83
+ "typecheck": "tsc --noEmit",
84
+ "test": "vitest run",
85
+ "size": "size-limit"
86
+ }
87
+ }