path-router-red 0.4.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 +427 -0
- package/dist/PathRouter/Container/ModalsContainer.d.ts +17 -0
- package/dist/PathRouter/Container/ModalsContainer.d.ts.map +1 -0
- package/dist/PathRouter/Container/ModalsContainer.js +39 -0
- package/dist/PathRouter/Container/ModalsContainer.js.map +1 -0
- package/dist/PathRouter/Container/RouterContainer.d.ts +16 -0
- package/dist/PathRouter/Container/RouterContainer.d.ts.map +1 -0
- package/dist/PathRouter/Container/RouterContainer.js +20 -0
- package/dist/PathRouter/Container/RouterContainer.js.map +1 -0
- package/dist/PathRouter/Container/index.d.ts +2 -0
- package/dist/PathRouter/Container/index.d.ts.map +1 -0
- package/dist/PathRouter/Container/index.js +18 -0
- package/dist/PathRouter/Container/index.js.map +1 -0
- package/dist/PathRouter/NavLink/NavLink.d.ts +40 -0
- package/dist/PathRouter/NavLink/NavLink.d.ts.map +1 -0
- package/dist/PathRouter/NavLink/NavLink.js +67 -0
- package/dist/PathRouter/NavLink/NavLink.js.map +1 -0
- package/dist/PathRouter/NavLink/index.d.ts +3 -0
- package/dist/PathRouter/NavLink/index.d.ts.map +1 -0
- package/dist/PathRouter/NavLink/index.js +6 -0
- package/dist/PathRouter/NavLink/index.js.map +1 -0
- package/dist/PathRouter/Provider/PathProvider.d.ts +10 -0
- package/dist/PathRouter/Provider/PathProvider.d.ts.map +1 -0
- package/dist/PathRouter/Provider/PathProvider.js +158 -0
- package/dist/PathRouter/Provider/PathProvider.js.map +1 -0
- package/dist/PathRouter/Provider/context.d.ts +3 -0
- package/dist/PathRouter/Provider/context.d.ts.map +1 -0
- package/dist/PathRouter/Provider/context.js +34 -0
- package/dist/PathRouter/Provider/context.js.map +1 -0
- package/dist/PathRouter/Provider/index.d.ts +3 -0
- package/dist/PathRouter/Provider/index.d.ts.map +1 -0
- package/dist/PathRouter/Provider/index.js +19 -0
- package/dist/PathRouter/Provider/index.js.map +1 -0
- package/dist/PathRouter/Provider/usePath.d.ts +15 -0
- package/dist/PathRouter/Provider/usePath.d.ts.map +1 -0
- package/dist/PathRouter/Provider/usePath.js +22 -0
- package/dist/PathRouter/Provider/usePath.js.map +1 -0
- package/dist/PathRouter/createPathRouter.d.ts +52 -0
- package/dist/PathRouter/createPathRouter.d.ts.map +1 -0
- package/dist/PathRouter/createPathRouter.js +72 -0
- package/dist/PathRouter/createPathRouter.js.map +1 -0
- package/dist/PathRouter/index.d.ts +41 -0
- package/dist/PathRouter/index.d.ts.map +1 -0
- package/dist/PathRouter/index.js +49 -0
- package/dist/PathRouter/index.js.map +1 -0
- package/dist/PathRouter/types.d.ts +91 -0
- package/dist/PathRouter/types.d.ts.map +1 -0
- package/dist/PathRouter/types.js +3 -0
- package/dist/PathRouter/types.js.map +1 -0
- package/dist/PathRouter/utils/clearSlash.d.ts +8 -0
- package/dist/PathRouter/utils/clearSlash.d.ts.map +1 -0
- package/dist/PathRouter/utils/clearSlash.js +21 -0
- package/dist/PathRouter/utils/clearSlash.js.map +1 -0
- package/dist/PathRouter/utils/createRoute.d.ts +16 -0
- package/dist/PathRouter/utils/createRoute.d.ts.map +1 -0
- package/dist/PathRouter/utils/createRoute.js +36 -0
- package/dist/PathRouter/utils/createRoute.js.map +1 -0
- package/dist/PathRouter/utils/index.d.ts +5 -0
- package/dist/PathRouter/utils/index.d.ts.map +1 -0
- package/dist/PathRouter/utils/index.js +21 -0
- package/dist/PathRouter/utils/index.js.map +1 -0
- package/dist/PathRouter/utils/parseSearch.d.ts +4 -0
- package/dist/PathRouter/utils/parseSearch.d.ts.map +1 -0
- package/dist/PathRouter/utils/parseSearch.js +15 -0
- package/dist/PathRouter/utils/parseSearch.js.map +1 -0
- package/dist/PathRouter/utils/setters.d.ts +9 -0
- package/dist/PathRouter/utils/setters.d.ts.map +1 -0
- package/dist/PathRouter/utils/setters.js +25 -0
- package/dist/PathRouter/utils/setters.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/PathRouter/Container/ModalsContainer.tsx +92 -0
- package/src/PathRouter/Container/RouterContainer.tsx +66 -0
- package/src/PathRouter/Container/index.ts +1 -0
- package/src/PathRouter/NavLink/NavLink.tsx +146 -0
- package/src/PathRouter/NavLink/index.ts +2 -0
- package/src/PathRouter/Provider/PathProvider.tsx +220 -0
- package/src/PathRouter/Provider/context.ts +33 -0
- package/src/PathRouter/Provider/index.ts +2 -0
- package/src/PathRouter/Provider/usePath.ts +21 -0
- package/src/PathRouter/createPathRouter.tsx +104 -0
- package/src/PathRouter/index.ts +79 -0
- package/src/PathRouter/readme.md +427 -0
- package/src/PathRouter/types.ts +139 -0
- package/src/PathRouter/utils/clearSlash.ts +16 -0
- package/src/PathRouter/utils/createRoute.ts +53 -0
- package/src/PathRouter/utils/index.ts +4 -0
- package/src/PathRouter/utils/parseSearch.ts +15 -0
- package/src/PathRouter/utils/setters.ts +8 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type AnchorHTMLAttributes,
|
|
4
|
+
type MouseEvent,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type Ref,
|
|
7
|
+
} from "react";
|
|
8
|
+
import type { NavigateOptions } from "react-router-dom";
|
|
9
|
+
|
|
10
|
+
import { usePath } from "../Provider/usePath";
|
|
11
|
+
import { clearSlash } from "../utils/clearSlash";
|
|
12
|
+
import type {
|
|
13
|
+
ModalNamesOf,
|
|
14
|
+
PathNamesOf,
|
|
15
|
+
RouterConfig,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
/** Internal — must match the splitter used by `PathProvider`. */
|
|
19
|
+
const modalSplitter = "/modal/";
|
|
20
|
+
|
|
21
|
+
export interface NavLinkProps<
|
|
22
|
+
C extends RouterConfig<any, any> = RouterConfig<any, any>,
|
|
23
|
+
> extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
24
|
+
/**
|
|
25
|
+
* Target page path. If omitted, the current page is preserved
|
|
26
|
+
* (useful when you only want to open a modal).
|
|
27
|
+
*/
|
|
28
|
+
to?: PathNamesOf<C>;
|
|
29
|
+
/** Modal to open on click. */
|
|
30
|
+
modal?: ModalNamesOf<C>;
|
|
31
|
+
/** Extra path segments appended after the modal name. */
|
|
32
|
+
modalBreadCrumbs?: string[];
|
|
33
|
+
/** Replace history entry instead of pushing a new one. */
|
|
34
|
+
replace?: boolean;
|
|
35
|
+
/** Extra options forwarded to the underlying `navigate`. */
|
|
36
|
+
navigateOptions?: NavigateOptions;
|
|
37
|
+
/** Class applied when this link matches the current location. */
|
|
38
|
+
activeClassName?: string;
|
|
39
|
+
children?: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isModifiedEvent = (e: MouseEvent<HTMLAnchorElement>) =>
|
|
43
|
+
e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
|
|
44
|
+
|
|
45
|
+
const NavLinkInner = forwardRef<HTMLAnchorElement, NavLinkProps>(
|
|
46
|
+
(
|
|
47
|
+
{
|
|
48
|
+
to,
|
|
49
|
+
modal,
|
|
50
|
+
modalBreadCrumbs,
|
|
51
|
+
replace,
|
|
52
|
+
navigateOptions,
|
|
53
|
+
className,
|
|
54
|
+
activeClassName,
|
|
55
|
+
onClick,
|
|
56
|
+
target,
|
|
57
|
+
children,
|
|
58
|
+
...rest
|
|
59
|
+
},
|
|
60
|
+
ref,
|
|
61
|
+
) => {
|
|
62
|
+
const { page, modal: modalCtx } = usePath();
|
|
63
|
+
|
|
64
|
+
/* ── Resolve the canonical href (always a real, shareable URL) ── */
|
|
65
|
+
const targetPagePath = to ? clearSlash(to as string) : page.path;
|
|
66
|
+
const tail = (modalBreadCrumbs || []).filter(Boolean).join("/");
|
|
67
|
+
const href = clearSlash(
|
|
68
|
+
modal
|
|
69
|
+
? `${targetPagePath}${modalSplitter}${modal}${tail ? `/${tail}` : ""}`
|
|
70
|
+
: targetPagePath,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/* ── Active state ── */
|
|
74
|
+
const isPageActive = !to || page.path === targetPagePath;
|
|
75
|
+
const isModalActive = modal
|
|
76
|
+
? modalCtx.isOpen && modalCtx.name === modal
|
|
77
|
+
: !modalCtx.isOpen || !to;
|
|
78
|
+
const isActive =
|
|
79
|
+
(!!to || !!modal) && isPageActive && (modal ? isModalActive : true);
|
|
80
|
+
|
|
81
|
+
/* ── Click handler: keep native behaviour for "open in new tab" ── */
|
|
82
|
+
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
83
|
+
if (onClick) onClick(e);
|
|
84
|
+
if (e.defaultPrevented) return;
|
|
85
|
+
|
|
86
|
+
// Let the browser do its thing for non-primary buttons, modifier keys,
|
|
87
|
+
// or links explicitly targeted to another window/frame.
|
|
88
|
+
if (
|
|
89
|
+
e.button !== 0 ||
|
|
90
|
+
isModifiedEvent(e) ||
|
|
91
|
+
(target && target !== "_self")
|
|
92
|
+
) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Nothing to navigate to — behave as a regular anchor.
|
|
97
|
+
if (!to && !modal) return;
|
|
98
|
+
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
|
|
101
|
+
const opts: NavigateOptions = { replace, ...navigateOptions };
|
|
102
|
+
// Single `navigate` call covers both "go to page" and
|
|
103
|
+
// "go to page + open modal" cases — the provider derives
|
|
104
|
+
// the modal state from the resulting pathname.
|
|
105
|
+
page.navigate(href as PathNamesOf<any>, opts);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<a
|
|
110
|
+
{...rest}
|
|
111
|
+
ref={ref}
|
|
112
|
+
href={href}
|
|
113
|
+
target={target}
|
|
114
|
+
onClick={handleClick}
|
|
115
|
+
aria-current={isActive ? "page" : undefined}
|
|
116
|
+
data-active={isActive || undefined}
|
|
117
|
+
className={`${className || ''} ${(isActive && activeClassName) || ''}`}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</a>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
NavLinkInner.displayName = "NavLink";
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Router-aware anchor.
|
|
129
|
+
*
|
|
130
|
+
* Renders a real `<a href>` (so right-click / "open in new tab" / SSR work)
|
|
131
|
+
* and intercepts the primary-button click to call `page.navigate` /
|
|
132
|
+
* open a modal through the `PathRouter` context.
|
|
133
|
+
*
|
|
134
|
+
* ```tsx
|
|
135
|
+
* <NavLink<typeof config> to="add">Add item</NavLink>
|
|
136
|
+
* <NavLink<typeof config> modal="confirm">Open confirm</NavLink>
|
|
137
|
+
* <NavLink<typeof config> to="users" modal="confirm" modalBreadCrumbs={["step-2"]}>
|
|
138
|
+
* Go to users + open confirm at step 2
|
|
139
|
+
* </NavLink>
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export const NavLink = NavLinkInner as <
|
|
143
|
+
C extends RouterConfig<any, any> = RouterConfig<any, any>,
|
|
144
|
+
>(
|
|
145
|
+
props: NavLinkProps<C> & { ref?: Ref<HTMLAnchorElement> },
|
|
146
|
+
) => React.ReactElement | null;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { type FC, type ReactNode, useCallback, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BrowserRouter,
|
|
4
|
+
type Location,
|
|
5
|
+
type NavigateOptions,
|
|
6
|
+
type To,
|
|
7
|
+
useLocation,
|
|
8
|
+
useNavigate,
|
|
9
|
+
} from "react-router-dom";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ModalState,
|
|
13
|
+
type PathContextType,
|
|
14
|
+
type SearchParams,
|
|
15
|
+
type SearchParamsState,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { clearSlash } from "../utils/clearSlash";
|
|
18
|
+
import { parseSearchParams } from "../utils/parseSearch";
|
|
19
|
+
import { PathContext } from "./context";
|
|
20
|
+
|
|
21
|
+
export interface PathProviderProps {
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const modalSplitter = "/modal/";
|
|
26
|
+
|
|
27
|
+
const InnerProvider: FC<PathProviderProps> = ({ children }) => {
|
|
28
|
+
const navigate = useNavigate();
|
|
29
|
+
const location = useLocation();
|
|
30
|
+
|
|
31
|
+
/** Derive page path & modal state from location.pathname (no extra render cycle). */
|
|
32
|
+
const { pagePath, modalState } = useMemo<{
|
|
33
|
+
pagePath: string;
|
|
34
|
+
modalState: ModalState;
|
|
35
|
+
}>(() => {
|
|
36
|
+
const [rawPagePath, modalPath] = location.pathname.split(modalSplitter) as [
|
|
37
|
+
string?,
|
|
38
|
+
string?,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const segments = (modalPath || "").split("/").filter(Boolean);
|
|
42
|
+
const name = segments[0];
|
|
43
|
+
const breadCrumbs = segments.slice(1);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
pagePath: clearSlash(rawPagePath || "/"),
|
|
47
|
+
modalState: {
|
|
48
|
+
name,
|
|
49
|
+
breadCrumbs,
|
|
50
|
+
path: segments.join("/"),
|
|
51
|
+
isOpen: !!name,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}, [location.pathname]);
|
|
55
|
+
|
|
56
|
+
/** Derive search params from location.search. */
|
|
57
|
+
const searchParams = useMemo<SearchParams>(() => {
|
|
58
|
+
const usp = new URLSearchParams(location.search);
|
|
59
|
+
const out: SearchParams = {};
|
|
60
|
+
for (const [key, value] of usp.entries()) {
|
|
61
|
+
const arr = out[key];
|
|
62
|
+
if (!arr) out[key] = [value];
|
|
63
|
+
else arr.push(value);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}, [location.search]);
|
|
67
|
+
|
|
68
|
+
const setModal = useCallback(
|
|
69
|
+
(name: string, breadCrumbs?: string[]) => {
|
|
70
|
+
const tail = (breadCrumbs || []).filter(Boolean).join("/");
|
|
71
|
+
const next = clearSlash(
|
|
72
|
+
`${pagePath}${modalSplitter}${name}${tail ? `/${tail}` : ""}`,
|
|
73
|
+
);
|
|
74
|
+
navigate(next);
|
|
75
|
+
},
|
|
76
|
+
[navigate, pagePath],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const closeModal = useCallback(() => {
|
|
80
|
+
navigate(clearSlash(pagePath));
|
|
81
|
+
}, [navigate, pagePath]);
|
|
82
|
+
|
|
83
|
+
const setPath = useCallback(
|
|
84
|
+
(to: To, options?: NavigateOptions) => {
|
|
85
|
+
if (typeof to === "string") {
|
|
86
|
+
const next = clearSlash(to);
|
|
87
|
+
if (next === location.pathname) return;
|
|
88
|
+
navigate(next, options);
|
|
89
|
+
} else {
|
|
90
|
+
if (
|
|
91
|
+
to.pathname &&
|
|
92
|
+
to.pathname === location.pathname &&
|
|
93
|
+
(to.search ?? "") === location.search &&
|
|
94
|
+
(to.hash ?? "") === location.hash
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
navigate(
|
|
99
|
+
{
|
|
100
|
+
...to,
|
|
101
|
+
pathname: to.pathname ? clearSlash(to.pathname) : to.pathname,
|
|
102
|
+
},
|
|
103
|
+
options,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[navigate, location.pathname, location.search, location.hash],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const pageNavigate = useCallback(
|
|
111
|
+
(path: string, options?: NavigateOptions) => {
|
|
112
|
+
setPath(path as To, options);
|
|
113
|
+
},
|
|
114
|
+
[setPath],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/** Merge: string -> set, array -> append. Preserves hash. */
|
|
118
|
+
const changeSearchParams = useCallback(
|
|
119
|
+
(next: SearchParamsState) => {
|
|
120
|
+
navigate({
|
|
121
|
+
pathname: location.pathname,
|
|
122
|
+
search: parseSearchParams(next, location as Location).toString(),
|
|
123
|
+
hash: location.hash,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
[navigate, location],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/** Replace fully: each provided key is overwritten with given value(s). */
|
|
130
|
+
const setSearchParams = useCallback(
|
|
131
|
+
(next: SearchParamsState) => {
|
|
132
|
+
const params = new URLSearchParams(location.search);
|
|
133
|
+
Object.entries(next).forEach(([key, value]) => {
|
|
134
|
+
params.delete(key);
|
|
135
|
+
if (typeof value === "string") {
|
|
136
|
+
params.append(key, value);
|
|
137
|
+
} else {
|
|
138
|
+
value.forEach((v) => params.append(key, v));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
navigate({
|
|
142
|
+
pathname: location.pathname,
|
|
143
|
+
search: params.toString(),
|
|
144
|
+
hash: location.hash,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
[navigate, location],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const deleteSearchParams = useCallback(
|
|
151
|
+
(key: string) => {
|
|
152
|
+
const params = new URLSearchParams(location.search);
|
|
153
|
+
params.delete(key);
|
|
154
|
+
navigate({
|
|
155
|
+
pathname: location.pathname,
|
|
156
|
+
search: params.toString(),
|
|
157
|
+
hash: location.hash,
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
[navigate, location],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const clearSearchParams = useCallback(() => {
|
|
164
|
+
navigate({
|
|
165
|
+
pathname: location.pathname,
|
|
166
|
+
search: "",
|
|
167
|
+
hash: location.hash,
|
|
168
|
+
});
|
|
169
|
+
}, [navigate, location]);
|
|
170
|
+
|
|
171
|
+
const value = useMemo<PathContextType>(
|
|
172
|
+
() => ({
|
|
173
|
+
page: {
|
|
174
|
+
path: pagePath,
|
|
175
|
+
navigate: pageNavigate,
|
|
176
|
+
isHavePrevHistory: location.key !== "default",
|
|
177
|
+
},
|
|
178
|
+
modal: {
|
|
179
|
+
...modalState,
|
|
180
|
+
open: setModal,
|
|
181
|
+
close: closeModal,
|
|
182
|
+
},
|
|
183
|
+
searchParams: {
|
|
184
|
+
params: searchParams,
|
|
185
|
+
change: changeSearchParams,
|
|
186
|
+
set: setSearchParams,
|
|
187
|
+
delete: deleteSearchParams,
|
|
188
|
+
clear: clearSearchParams,
|
|
189
|
+
},
|
|
190
|
+
defaultLocation: location as Location,
|
|
191
|
+
}),
|
|
192
|
+
[
|
|
193
|
+
pagePath,
|
|
194
|
+
pageNavigate,
|
|
195
|
+
location,
|
|
196
|
+
modalState,
|
|
197
|
+
setModal,
|
|
198
|
+
closeModal,
|
|
199
|
+
searchParams,
|
|
200
|
+
changeSearchParams,
|
|
201
|
+
setSearchParams,
|
|
202
|
+
deleteSearchParams,
|
|
203
|
+
clearSearchParams,
|
|
204
|
+
],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return <PathContext.Provider value={value}>{children}</PathContext.Provider>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Wraps the application with `BrowserRouter` and the `PathContext`.
|
|
212
|
+
* Place this near the root of your component tree.
|
|
213
|
+
*/
|
|
214
|
+
export const PathProvider: FC<PathProviderProps> = ({ children }) => {
|
|
215
|
+
return (
|
|
216
|
+
<BrowserRouter>
|
|
217
|
+
<InnerProvider>{children}</InnerProvider>
|
|
218
|
+
</BrowserRouter>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { type PathContextType } from "../types";
|
|
3
|
+
|
|
4
|
+
const defaultValues: PathContextType = {
|
|
5
|
+
page: {
|
|
6
|
+
path: "",
|
|
7
|
+
navigate: () => {},
|
|
8
|
+
isHavePrevHistory: false,
|
|
9
|
+
},
|
|
10
|
+
modal: {
|
|
11
|
+
open: () => {},
|
|
12
|
+
close: () => {},
|
|
13
|
+
path: "",
|
|
14
|
+
breadCrumbs: [],
|
|
15
|
+
isOpen: false,
|
|
16
|
+
},
|
|
17
|
+
searchParams: {
|
|
18
|
+
params: {},
|
|
19
|
+
change: () => {},
|
|
20
|
+
set: () => {},
|
|
21
|
+
clear: () => {},
|
|
22
|
+
delete: () => {},
|
|
23
|
+
},
|
|
24
|
+
defaultLocation: {
|
|
25
|
+
hash: "",
|
|
26
|
+
key: "",
|
|
27
|
+
pathname: "",
|
|
28
|
+
search: "",
|
|
29
|
+
state: {},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const PathContext = createContext<PathContextType>(defaultValues);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { PathContext } from "./context";
|
|
3
|
+
import type { PathContextType, RouterConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Access the router context.
|
|
7
|
+
*
|
|
8
|
+
* Pass `typeof yourConfig` as the generic to get fully typed
|
|
9
|
+
* `page.navigate(...)` and `modal.open(...)` arguments:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const { page, modal } = usePath<typeof config>();
|
|
13
|
+
* page.navigate("add"); // typed
|
|
14
|
+
* modal.open("test"); // typed
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const usePath = <
|
|
18
|
+
C extends RouterConfig<any, any> = RouterConfig<any, any>,
|
|
19
|
+
>(): PathContextType<C> => {
|
|
20
|
+
return useContext(PathContext) as unknown as PathContextType<C>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { type ReactNode, type Ref, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import { PathContext } from "./Provider/context";
|
|
4
|
+
import { PathProvider as BasePathProvider } from "./Provider/PathProvider";
|
|
5
|
+
import { PathRouterContainer as BasePathRouterContainer } from "./Container/RouterContainer";
|
|
6
|
+
import { NavLink as BaseNavLink, type NavLinkProps } from "./NavLink";
|
|
7
|
+
import type {
|
|
8
|
+
ModalNamesOf,
|
|
9
|
+
ModalWrapperComponent,
|
|
10
|
+
PathContextType,
|
|
11
|
+
PathNamesOf,
|
|
12
|
+
RouterConfig,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bound `PathRouterContainer` props — `config` is captured by the factory,
|
|
17
|
+
* so the consumer only needs to pass presentation-related options.
|
|
18
|
+
*/
|
|
19
|
+
export interface BoundPathRouterContainerProps {
|
|
20
|
+
ModalWrapper?: ModalWrapperComponent;
|
|
21
|
+
fallback?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BoundNavLinkProps<
|
|
25
|
+
C extends RouterConfig<any, any>,
|
|
26
|
+
> extends NavLinkProps<C> {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a router instance bound to a concrete config.
|
|
30
|
+
*
|
|
31
|
+
* All returned helpers are pre-typed against the supplied config — no need
|
|
32
|
+
* to thread `typeof config` through every call site.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { setPage, setModal, createPathRouter } from "@/modules/PathRouter";
|
|
36
|
+
*
|
|
37
|
+
* const config = {
|
|
38
|
+
* pages: { home: setPage({ component: Home }) },
|
|
39
|
+
* modals: { test: setModal({ component: TestModal }) },
|
|
40
|
+
* } as const;
|
|
41
|
+
*
|
|
42
|
+
* export const {
|
|
43
|
+
* PathProvider,
|
|
44
|
+
* PathRouterContainer,
|
|
45
|
+
* usePath,
|
|
46
|
+
* NavLink,
|
|
47
|
+
* getPath,
|
|
48
|
+
* getModal,
|
|
49
|
+
* } = createPathRouter(config);
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const createPathRouter = <const C extends RouterConfig<any, any>>(
|
|
53
|
+
config: C,
|
|
54
|
+
) => {
|
|
55
|
+
/** Typed `usePath` — `page.navigate` / `modal.open` know your routes. */
|
|
56
|
+
const usePath = (): PathContextType<C> =>
|
|
57
|
+
useContext(PathContext) as unknown as PathContextType<C>;
|
|
58
|
+
|
|
59
|
+
/** Container with `config` already injected. */
|
|
60
|
+
const PathRouterContainer: React.FC<BoundPathRouterContainerProps> = (
|
|
61
|
+
props,
|
|
62
|
+
) => <BasePathRouterContainer config={config} {...props} />;
|
|
63
|
+
|
|
64
|
+
/** Typed `NavLink` — `to` / `modal` are autocompleted from your config. */
|
|
65
|
+
const NavLink = BaseNavLink as unknown as (
|
|
66
|
+
props: BoundNavLinkProps<C> & { ref?: Ref<HTMLAnchorElement> },
|
|
67
|
+
) => React.ReactElement | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Identity helper that constrains its argument to a valid page path
|
|
71
|
+
* for this config. Use it where you need a typed path literal:
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* page.navigate(getPath("home"));
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
const getPath = <P extends PathNamesOf<C>>(path: P): P => path;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Identity helper that constrains its argument to a valid modal name
|
|
81
|
+
* for this config.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* modal.open(getModal("test"));
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
const getModal = <M extends ModalNamesOf<C>>(name: M): M => name;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
/** `BrowserRouter` + context provider — does not depend on the config. */
|
|
91
|
+
PathProvider: BasePathProvider,
|
|
92
|
+
PathRouterContainer,
|
|
93
|
+
usePath,
|
|
94
|
+
NavLink,
|
|
95
|
+
getPath,
|
|
96
|
+
getModal,
|
|
97
|
+
/** The original config, re-exported for convenience. */
|
|
98
|
+
config,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type PathRouter<C extends RouterConfig<any, any>> = ReturnType<
|
|
103
|
+
typeof createPathRouter<C>
|
|
104
|
+
>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PathRouter — public API.
|
|
3
|
+
*
|
|
4
|
+
* Everything that depends on a concrete config is produced by
|
|
5
|
+
* `createPathRouter(config)`. The package itself only exposes:
|
|
6
|
+
*
|
|
7
|
+
* - `setPage` / `setModal` — config builders;
|
|
8
|
+
* - `createPathRouter` — the factory;
|
|
9
|
+
* - `clearSlash` — path normalizer;
|
|
10
|
+
* - generic helper types — must be parametrised with `typeof config`;
|
|
11
|
+
* - plugin / data types — for `ModalWrapper` authors and modal
|
|
12
|
+
* components (`ModalProps`, etc.).
|
|
13
|
+
*
|
|
14
|
+
* `PathProvider`, `PathRouterContainer`, `usePath`, `NavLink`, `getPath`,
|
|
15
|
+
* `getModal` are intentionally **not** re-exported — obtain them from
|
|
16
|
+
* `createPathRouter(config)`.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { setPage, setModal, createPathRouter } from "@/modules/PathRouter";
|
|
20
|
+
*
|
|
21
|
+
* const config = {
|
|
22
|
+
* pages: { home: setPage({ component: Home }) },
|
|
23
|
+
* modals: { test: setModal({ component: TestModal }) },
|
|
24
|
+
* } as const;
|
|
25
|
+
*
|
|
26
|
+
* export const {
|
|
27
|
+
* PathProvider,
|
|
28
|
+
* PathRouterContainer,
|
|
29
|
+
* usePath,
|
|
30
|
+
* NavLink,
|
|
31
|
+
* getPath,
|
|
32
|
+
* getModal,
|
|
33
|
+
* } = createPathRouter(config);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/* Config builders */
|
|
38
|
+
export { setPage, setModal } from "./utils/setters";
|
|
39
|
+
|
|
40
|
+
/* The factory — the only way to get config-bound router pieces */
|
|
41
|
+
export { createPathRouter } from "./createPathRouter";
|
|
42
|
+
export type {
|
|
43
|
+
BoundPathRouterContainerProps,
|
|
44
|
+
BoundNavLinkProps,
|
|
45
|
+
PathRouter,
|
|
46
|
+
} from "./createPathRouter";
|
|
47
|
+
|
|
48
|
+
/* Path normalizer */
|
|
49
|
+
export { clearSlash } from "./utils/clearSlash";
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* Public types.
|
|
53
|
+
*
|
|
54
|
+
* Note on config-dependent generics: `PathNamesOf<C>` and `ModalNamesOf<C>`
|
|
55
|
+
* REQUIRE the config generic — there is no default. Use them as
|
|
56
|
+
* `PathNamesOf<typeof config>` etc.
|
|
57
|
+
*
|
|
58
|
+
* `PathContextType` is intentionally not re-exported — get a typed context
|
|
59
|
+
* shape via the `usePath` returned by `createPathRouter(config)`.
|
|
60
|
+
*/
|
|
61
|
+
export type {
|
|
62
|
+
/* Config shape */
|
|
63
|
+
RouterConfig,
|
|
64
|
+
PageData,
|
|
65
|
+
ModalData,
|
|
66
|
+
/* Props passed to a modal component */
|
|
67
|
+
ModalProps,
|
|
68
|
+
/* Config-dependent helpers (require <typeof config>) */
|
|
69
|
+
PathNamesOf,
|
|
70
|
+
ModalNamesOf,
|
|
71
|
+
/* Context sub-shapes */
|
|
72
|
+
ModalState,
|
|
73
|
+
SearchParams,
|
|
74
|
+
SearchParamsState,
|
|
75
|
+
/* `ModalWrapper` plugin contract */
|
|
76
|
+
ModalWrapperComponent,
|
|
77
|
+
ModalWrapperProps,
|
|
78
|
+
ModalWrapperRef,
|
|
79
|
+
} from "./types";
|