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
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# PathRouter
|
|
2
|
+
|
|
3
|
+
A small, type-safe routing layer built on top of `react-router-dom` that adds:
|
|
4
|
+
|
|
5
|
+
- A **declarative config** for pages and modals (`setPage` / `setModal`).
|
|
6
|
+
- A **factory** (`createPathRouter`) that binds your config to fully-typed router pieces — no manual generics on every call site.
|
|
7
|
+
- A **single React context** exposing the current page, the current modal and the search params, with imperative helpers (`navigate`, `open`, `close`, `set`, `change`, `delete`, `clear`).
|
|
8
|
+
- **URL-driven modals**: a modal is represented as a segment after `/modal/` inside the path, so it survives reloads, deep links and back/forward navigation.
|
|
9
|
+
- An optional **`ModalWrapper`** plugin (e.g. an animated popup) that can intercept the close action via a forwarded ref.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File layout
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
PathRouter/
|
|
17
|
+
├── index.ts # Public API (factory + builders + types)
|
|
18
|
+
├── createPathRouter.tsx # The factory itself
|
|
19
|
+
├── types.ts # All public types
|
|
20
|
+
├── Container/
|
|
21
|
+
│ ├── RouterContainer.tsx # Renders <Routes> for pages + <ModalsContainer>
|
|
22
|
+
│ ├── ModalsContainer.tsx # Renders the currently open modal
|
|
23
|
+
│ └── index.ts
|
|
24
|
+
├── Provider/
|
|
25
|
+
│ ├── PathProvider.tsx # BrowserRouter + PathContext provider
|
|
26
|
+
│ ├── context.ts # React context object
|
|
27
|
+
│ ├── usePath.ts # Internal hook (factory wraps it for users)
|
|
28
|
+
│ └── index.ts
|
|
29
|
+
├── NavLink/
|
|
30
|
+
│ ├── NavLink.tsx # Internal NavLink (factory wraps it for users)
|
|
31
|
+
│ └── index.ts
|
|
32
|
+
└── utils/
|
|
33
|
+
├── setters.ts # setPage / setModal helpers
|
|
34
|
+
├── createRoute.ts # Flattens the nested page config
|
|
35
|
+
├── clearSlash.ts # Path normalization
|
|
36
|
+
├── parseSearch.ts # Search-params merge helper
|
|
37
|
+
└── index.ts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// src/config/route.ts
|
|
46
|
+
import { setPage, setModal } from "@/modules/PathRouter";
|
|
47
|
+
import { HomePage, AddPage, NotFoundPage } from "@/Pages";
|
|
48
|
+
import { TestModal } from "@/Modals/Test";
|
|
49
|
+
|
|
50
|
+
export const route = {
|
|
51
|
+
pages: {
|
|
52
|
+
home: setPage({ component: HomePage }),
|
|
53
|
+
add: setPage({ component: AddPage }),
|
|
54
|
+
"*": setPage({ component: NotFoundPage }),
|
|
55
|
+
},
|
|
56
|
+
modals: {
|
|
57
|
+
test: setModal({ component: TestModal }),
|
|
58
|
+
},
|
|
59
|
+
} as const;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/containers/Router/PathProvider.tsx
|
|
64
|
+
import { route } from "@/config";
|
|
65
|
+
import { createPathRouter } from "@/modules/PathRouter";
|
|
66
|
+
|
|
67
|
+
export const {
|
|
68
|
+
PathProvider,
|
|
69
|
+
PathRouterContainer,
|
|
70
|
+
usePath,
|
|
71
|
+
NavLink,
|
|
72
|
+
getPath,
|
|
73
|
+
getModal,
|
|
74
|
+
} = createPathRouter(route);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// somewhere near the root
|
|
79
|
+
import { PathProvider, PathRouterContainer } from "@/containers/Router";
|
|
80
|
+
|
|
81
|
+
<PathProvider>
|
|
82
|
+
<PathRouterContainer fallback={<Spinner />} />
|
|
83
|
+
</PathProvider>;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// any component
|
|
88
|
+
import { usePath, NavLink } from "@/containers/Router";
|
|
89
|
+
|
|
90
|
+
const Foo = () => {
|
|
91
|
+
const { page, modal } = usePath(); // no <typeof config> generic needed!
|
|
92
|
+
page.navigate("add"); // ✓ autocompleted
|
|
93
|
+
modal.open("test"); // ✓ autocompleted
|
|
94
|
+
return <NavLink to="home" modal="test">Open</NavLink>;
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
### 1. Configuration via `setPage` / `setModal`
|
|
103
|
+
|
|
104
|
+
The config is a plain object with two sections — `pages` and `modals`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { setPage, setModal } from "@/modules/PathRouter";
|
|
108
|
+
|
|
109
|
+
export const route = {
|
|
110
|
+
pages: {
|
|
111
|
+
"/": setPage({ component: HomePage }),
|
|
112
|
+
add: setPage({ component: AddItemPage }),
|
|
113
|
+
users: {
|
|
114
|
+
"/": setPage({ component: UsersListPage }),
|
|
115
|
+
":id": setPage({ component: UserPage }),
|
|
116
|
+
},
|
|
117
|
+
modules: {
|
|
118
|
+
...setPage({ component: ModulesIndexPage }), // page at /modules
|
|
119
|
+
routing: setPage({ component: RoutingPage }), // page at /modules/routing
|
|
120
|
+
"*": setPage({ component: RoutingPage }), // page at /modules/*
|
|
121
|
+
},
|
|
122
|
+
"*": setPage({ component: NotFoundPage }),
|
|
123
|
+
},
|
|
124
|
+
modals: {
|
|
125
|
+
test: setModal({ component: TestModal }),
|
|
126
|
+
confirm: setModal({ component: ConfirmModal }),
|
|
127
|
+
},
|
|
128
|
+
} as const;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `setPage({ component, redirect? })` wraps the value as `{ data: {...} }`. The `data` field marks the node as a renderable page, but **does not stop** the route builder from descending — children of the same node are still discovered.
|
|
132
|
+
- Pages can be **nested** as plain objects — `createRoute` walks the tree and produces a flat `[{ pathName, data }]` list (see `utils/createRoute.ts`).
|
|
133
|
+
- A node may **simultaneously be a page and a container** for child routes. Spread the result of `setPage` into the node to attach a component at that path while keeping nested keys:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
modules: {
|
|
137
|
+
...setPage({ component: ModulesIndexPage }), // /modules
|
|
138
|
+
routing: setPage({ component: RoutingPage }), // /modules/routing
|
|
139
|
+
"*": setPage({ component: RoutingPage }), // /modules/*
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This is what enables breadcrumb-style hierarchies where each ancestor segment is itself a page.
|
|
144
|
+
- Recursion into a node stops only when the node itself carries `component` or `redirect` at its top level (i.e. it is a bare leaf, not a `setPage(...)` result). `setPage` puts those fields under `data`, so spreading it never blocks descent.
|
|
145
|
+
- A page with `redirect` (or no `component`) becomes a `<Navigate to={redirect || "/"} replace />`.
|
|
146
|
+
- `setModal({ component })` is just an identity helper that preserves literal types for inference.
|
|
147
|
+
- `as const` is **required** — without it TS widens string keys to `string` and you lose autocompletion.
|
|
148
|
+
|
|
149
|
+
### 2. The `createPathRouter` factory
|
|
150
|
+
|
|
151
|
+
`createPathRouter(route)` captures your config once and returns everything pre-bound:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const {
|
|
155
|
+
PathProvider, // BrowserRouter + context
|
|
156
|
+
PathRouterContainer, // renders pages + modals (config injected)
|
|
157
|
+
usePath, // typed hook — no <typeof config> needed
|
|
158
|
+
NavLink, // typed link — no <typeof config> needed
|
|
159
|
+
getPath, // identity helper: <P extends PathNamesOf<C>>(p: P) => p
|
|
160
|
+
getModal, // identity helper: <M extends ModalNamesOf<C>>(m: M) => m
|
|
161
|
+
config, // the original config, re-exported
|
|
162
|
+
} = createPathRouter(route);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Why re-exports rather than direct imports?
|
|
166
|
+
|
|
167
|
+
- TypeScript cannot infer generics from a literal `typeof route` _unless_ you pass it explicitly each time. The factory passes it once for you.
|
|
168
|
+
- Every consumer of the returned API gets autocompletion of routes / modal names with **zero ceremony**.
|
|
169
|
+
- The package itself stays generic and reusable; the binding lives in your app code.
|
|
170
|
+
|
|
171
|
+
> The factory return values are not re-exported from `@/modules/PathRouter`. The only way to obtain `PathProvider`, `PathRouterContainer`, `usePath`, `NavLink`, `getPath`, `getModal` is via `createPathRouter(config)`.
|
|
172
|
+
|
|
173
|
+
### 3. Mounting
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { PathProvider, PathRouterContainer } from "@/containers/Router";
|
|
177
|
+
|
|
178
|
+
<PathProvider>
|
|
179
|
+
<PathRouterContainer
|
|
180
|
+
ModalWrapper={MyModalWrapper} // optional
|
|
181
|
+
fallback={<Spinner />} // optional Suspense fallback
|
|
182
|
+
/>
|
|
183
|
+
</PathProvider>;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- `PathProvider` mounts a `BrowserRouter` and an inner provider that derives the page path, the modal state and the search params from `useLocation()` (`Provider/PathProvider.tsx`).
|
|
187
|
+
- `PathRouterContainer` renders the pages inside `<Suspense>` and, if a modal is open, mounts `ModalsContainer` next to the page tree. `config` is already injected by the factory — you only pass `ModalWrapper` / `fallback`.
|
|
188
|
+
|
|
189
|
+
### 4. URL shape
|
|
190
|
+
|
|
191
|
+
A URL is split on the literal **`/modal/`** separator:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
/users/42/modal/confirm/extra-crumb?tab=info
|
|
195
|
+
└── page path ──┘ └─ modal ──┘
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In `PathProvider`:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const [rawPagePath, modalPath] = location.pathname.split("/modal/");
|
|
202
|
+
const segments = (modalPath || "").split("/").filter(Boolean);
|
|
203
|
+
const name = segments[0]; // "confirm"
|
|
204
|
+
const breadCrumbs = segments.slice(1); // ["extra-crumb"]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
So:
|
|
208
|
+
|
|
209
|
+
- `page.path` always points to the page route the user is on, regardless of whether a modal is open.
|
|
210
|
+
- `modal.name` is the first segment after `/modal/`.
|
|
211
|
+
- `modal.breadCrumbs` are additional path segments that the modal can use for its own internal navigation/steps.
|
|
212
|
+
- `modal.isOpen` is `true` iff `modal.name` is set.
|
|
213
|
+
|
|
214
|
+
This means modals are **bookmarkable and shareable** out of the box and the browser back button closes the modal naturally.
|
|
215
|
+
|
|
216
|
+
### 5. The `usePath` hook (from the factory)
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { usePath } from "@/containers/Router";
|
|
220
|
+
|
|
221
|
+
const { page, modal, searchParams } = usePath(); // already typed!
|
|
222
|
+
|
|
223
|
+
page.path; // current page pathname (no /modal/... suffix)
|
|
224
|
+
page.navigate("add"); // ✓ typed against config.pages
|
|
225
|
+
page.isHavePrevHistory; // true if history.key !== "default"
|
|
226
|
+
|
|
227
|
+
modal.isOpen;
|
|
228
|
+
modal.name; // current modal key
|
|
229
|
+
modal.breadCrumbs; // string[] after the modal name
|
|
230
|
+
modal.path; // "<name>/<crumb1>/<crumb2>"
|
|
231
|
+
modal.open("confirm", ["step-2"]); // ✓ typed against config.modals
|
|
232
|
+
modal.close();
|
|
233
|
+
|
|
234
|
+
searchParams.params; // Record<string, string[]>
|
|
235
|
+
searchParams.change({ tab: "info" }); // merge (string=set, string[]=append)
|
|
236
|
+
searchParams.set({ tab: ["a", "b"] }); // replace per-key
|
|
237
|
+
searchParams.delete("tab");
|
|
238
|
+
searchParams.clear();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 6. `getPath` / `getModal` — typed identity helpers
|
|
242
|
+
|
|
243
|
+
When you need a typed path or modal-name literal somewhere outside JSX (e.g. inside a side-effect, a redux thunk, a `redirect` field of another page), use the identity helpers returned by the factory:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { getPath, getModal } from "@/containers/Router";
|
|
247
|
+
|
|
248
|
+
const target = getPath("home"); // type: "home"
|
|
249
|
+
const which = getModal("test"); // type: "test"
|
|
250
|
+
|
|
251
|
+
// Compile-time error: argument is not assignable to PathNamesOf<typeof route>
|
|
252
|
+
const bad = getPath("does-not-exist");
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
These replace the previous `export type PathNames = NestedKeyOf<typeof routes, "data">` pattern that is impossible to express from inside an isolated package.
|
|
256
|
+
|
|
257
|
+
If you really need the type itself (e.g. as a function parameter), it is still available via `typeof getPath`:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
type PathNames = Parameters<typeof getPath>[0];
|
|
261
|
+
type ModalNames = Parameters<typeof getModal>[0];
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
…or use the package-level generics with an explicit config:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import type { PathNamesOf, ModalNamesOf } from "@/modules/PathRouter";
|
|
268
|
+
import type { route } from "@/config";
|
|
269
|
+
|
|
270
|
+
type PathNames = PathNamesOf<typeof route>;
|
|
271
|
+
type ModalNames = ModalNamesOf<typeof route>;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 7. `NavLink` (from the factory)
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { NavLink } from "@/containers/Router";
|
|
278
|
+
|
|
279
|
+
<NavLink to="home">Home</NavLink>
|
|
280
|
+
<NavLink modal="test">Open test modal</NavLink>
|
|
281
|
+
<NavLink to="users" modal="confirm" modalBreadCrumbs={["step-2"]}>
|
|
282
|
+
Users + confirm at step 2
|
|
283
|
+
</NavLink>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- Renders a real `<a href>` (right-click / “open in new tab” / SSR work as expected).
|
|
287
|
+
- Intercepts the primary-button click and routes through `page.navigate(...)`.
|
|
288
|
+
- Adds `aria-current="page"` and `data-active` when active; you can override the active class via `activeClassName`.
|
|
289
|
+
|
|
290
|
+
### 8. Page rendering (`RouterContainer.tsx`)
|
|
291
|
+
|
|
292
|
+
- Calls `createRoute(config)` once (memoised) to flatten the page tree.
|
|
293
|
+
- Renders a single `<Routes>` switch with one `<Route>` per leaf.
|
|
294
|
+
- If a leaf has no `component` or has a `redirect`, the element becomes `<Navigate to={redirect || "/"} replace />`.
|
|
295
|
+
- The whole switch is wrapped in `<Suspense fallback={fallback}>` so lazy components work transparently.
|
|
296
|
+
- Modals are rendered as a **sibling** of `<Routes>`, only when `modal.isOpen` — they overlay the current page rather than replacing it.
|
|
297
|
+
|
|
298
|
+
### 9. Modal rendering (`ModalsContainer.tsx`)
|
|
299
|
+
|
|
300
|
+
- Builds a virtual location `"<pagePath>/<modalName>"` (normalized via `clearSlash`) and feeds it to a dedicated `<Routes location={routesLocation}>`. This is what makes the modal aware of the current page path.
|
|
301
|
+
- Each modal route is registered at `"<pagePath>/<modalName>"`, so modals can be **page-scoped** if needed.
|
|
302
|
+
- If the URL contains a modal name that does not exist in the config (no matching component), the container calls `modal.close()` automatically — broken/stale modal links self-heal.
|
|
303
|
+
- If a `ModalWrapper` is provided, it is rendered with `{ modalName, isOpen, onClose, children }` and a forwarded ref. When the user triggers close, the container prefers `ref.current.handleCloseWithAnimation()` (so the wrapper can run an exit animation), and only falls back to the raw `close()` if that method is not exposed.
|
|
304
|
+
|
|
305
|
+
The `ModalWrapper` contract:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
interface ModalWrapperRef {
|
|
309
|
+
handleCloseWithAnimation: () => void;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface ModalWrapperProps {
|
|
313
|
+
modalName?: string;
|
|
314
|
+
isOpen: boolean;
|
|
315
|
+
onClose: () => void;
|
|
316
|
+
children?: ReactNode;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
type ModalWrapperComponent = ForwardRefExoticComponent<
|
|
320
|
+
ModalWrapperProps & RefAttributes<ModalWrapperRef>
|
|
321
|
+
>;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Modal components themselves receive `ModalProps` (`{ onClose: () => void }`):
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
import type { FC } from "react";
|
|
328
|
+
import type { ModalProps } from "@/modules/PathRouter";
|
|
329
|
+
|
|
330
|
+
export const TestModal: FC<ModalProps> = ({ onClose }) => (
|
|
331
|
+
<button onClick={onClose}>Close</button>
|
|
332
|
+
);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 10. Path normalization (`clearSlash`)
|
|
336
|
+
|
|
337
|
+
All internal `navigate(...)` calls go through `clearSlash`:
|
|
338
|
+
|
|
339
|
+
- collapses repeated slashes (`a//b` → `a/b`);
|
|
340
|
+
- guarantees a single leading slash;
|
|
341
|
+
- strips trailing slashes (root `/` stays as `/`).
|
|
342
|
+
|
|
343
|
+
This keeps the URL canonical regardless of how the caller composed it.
|
|
344
|
+
|
|
345
|
+
### 11. Search params
|
|
346
|
+
|
|
347
|
+
`PathProvider` derives `searchParams` from `location.search` and exposes four helpers:
|
|
348
|
+
|
|
349
|
+
| Method | Behaviour |
|
|
350
|
+
| -------- | ------------------------------------------------------------------------- |
|
|
351
|
+
| `change` | Merge: `string` value → set the key; `string[]` value → append values. |
|
|
352
|
+
| `set` | Replace each provided key: deletes existing values, then writes new ones. |
|
|
353
|
+
| `delete` | Removes a key entirely. |
|
|
354
|
+
| `clear` | Removes every search param. |
|
|
355
|
+
|
|
356
|
+
All mutations preserve `location.hash`.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Public API of `@/modules/PathRouter`
|
|
361
|
+
|
|
362
|
+
The package exposes only what cannot depend on a concrete config:
|
|
363
|
+
|
|
364
|
+
### Builders / factory / utilities
|
|
365
|
+
|
|
366
|
+
- `setPage`, `setModal` — config builders.
|
|
367
|
+
- `createPathRouter(config)` — returns `{ PathProvider, PathRouterContainer, usePath, NavLink, getPath, getModal, config }`.
|
|
368
|
+
- `clearSlash` — path normalizer.
|
|
369
|
+
|
|
370
|
+
### Types — config-independent
|
|
371
|
+
|
|
372
|
+
- `RouterConfig`, `PageData`, `ModalData`
|
|
373
|
+
- `ModalProps` — props passed to a modal component.
|
|
374
|
+
- `ModalState`, `SearchParams`, `SearchParamsState`
|
|
375
|
+
- `ModalWrapperComponent`, `ModalWrapperProps`, `ModalWrapperRef`
|
|
376
|
+
- `BoundPathRouterContainerProps`, `BoundNavLinkProps<C>`, `PathRouter<C>`
|
|
377
|
+
|
|
378
|
+
### Types — config-dependent (must be parametrised)
|
|
379
|
+
|
|
380
|
+
- `PathNamesOf<C>` — must be used as `PathNamesOf<typeof route>`.
|
|
381
|
+
- `ModalNamesOf<C>` — must be used as `ModalNamesOf<typeof route>`.
|
|
382
|
+
|
|
383
|
+
> `PathProvider`, `PathRouterContainer`, `usePath`, `NavLink`, `getPath`, `getModal` are **deliberately not exported** from the package — obtain them from `createPathRouter(config)`.
|
|
384
|
+
> `PathContextType` is also not re-exported; the typed shape is available via the return type of the factory's `usePath`.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Minimal end-to-end example
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
import { setPage, setModal, createPathRouter } from "@/modules/PathRouter";
|
|
392
|
+
|
|
393
|
+
const route = {
|
|
394
|
+
pages: {
|
|
395
|
+
"/": setPage({ component: HomePage }),
|
|
396
|
+
add: setPage({ component: AddItemPage }),
|
|
397
|
+
"*": setPage({ redirect: "/" }),
|
|
398
|
+
},
|
|
399
|
+
modals: {
|
|
400
|
+
confirm: setModal({ component: ConfirmModal }),
|
|
401
|
+
},
|
|
402
|
+
} as const;
|
|
403
|
+
|
|
404
|
+
export const {
|
|
405
|
+
PathProvider,
|
|
406
|
+
PathRouterContainer,
|
|
407
|
+
usePath,
|
|
408
|
+
NavLink,
|
|
409
|
+
getPath,
|
|
410
|
+
getModal,
|
|
411
|
+
} = createPathRouter(route);
|
|
412
|
+
|
|
413
|
+
export const App = () => (
|
|
414
|
+
<PathProvider>
|
|
415
|
+
<PathRouterContainer />
|
|
416
|
+
</PathProvider>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const SomeButton = () => {
|
|
420
|
+
const { page, modal } = usePath();
|
|
421
|
+
return (
|
|
422
|
+
<button onClick={() => modal.open("confirm")}>
|
|
423
|
+
Open confirm (current page stays: {page.path})
|
|
424
|
+
</button>
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ModalData, ModalWrapperComponent } from "../types";
|
|
3
|
+
export interface ModalContainerProps {
|
|
4
|
+
paths: {
|
|
5
|
+
pathName: string;
|
|
6
|
+
data: ModalData;
|
|
7
|
+
}[];
|
|
8
|
+
/**
|
|
9
|
+
* Optional wrapper around the modal contents (e.g. an animated popup).
|
|
10
|
+
* If omitted, the modal component is rendered directly.
|
|
11
|
+
*/
|
|
12
|
+
ModalWrapper?: ModalWrapperComponent;
|
|
13
|
+
/** Fallback shown by `Suspense` while a lazy modal is loading. */
|
|
14
|
+
fallback?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare const ModalsContainer: React.FC<ModalContainerProps>;
|
|
17
|
+
//# sourceMappingURL=ModalsContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalsContainer.d.ts","sourceRoot":"","sources":["../../../src/PathRouter/Container/ModalsContainer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgD,MAAM,OAAO,CAAC;AAKrE,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,EAAE,CAAC;IAC/C;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAqEzD,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModalsContainer = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_router_dom_1 = require("react-router-dom");
|
|
7
|
+
const usePath_1 = require("../Provider/usePath");
|
|
8
|
+
const clearSlash_1 = require("../utils/clearSlash");
|
|
9
|
+
const ModalsContainer = ({ paths, ModalWrapper, fallback = null, }) => {
|
|
10
|
+
const { page, modal } = (0, usePath_1.usePath)();
|
|
11
|
+
const { name: modalName, isOpen, close } = modal;
|
|
12
|
+
const modalRef = (0, react_1.useRef)(null);
|
|
13
|
+
const hasMatchingComponent = !!modalName &&
|
|
14
|
+
paths.some(({ pathName, data }) => pathName === modalName && !!data.component);
|
|
15
|
+
(0, react_1.useEffect)(() => {
|
|
16
|
+
if (isOpen && !hasMatchingComponent) {
|
|
17
|
+
close();
|
|
18
|
+
}
|
|
19
|
+
}, [isOpen, hasMatchingComponent, close]);
|
|
20
|
+
if (!modalName)
|
|
21
|
+
return null;
|
|
22
|
+
const routesLocation = (0, clearSlash_1.clearSlash)(`${page.path}/${modalName}`);
|
|
23
|
+
const handleClose = () => modalRef.current?.handleCloseWithAnimation
|
|
24
|
+
? modalRef.current.handleCloseWithAnimation()
|
|
25
|
+
: close();
|
|
26
|
+
const content = ((0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: fallback, children: (0, jsx_runtime_1.jsx)(react_router_dom_1.Routes, { location: routesLocation, children: paths.map(({ pathName, data }, i) => {
|
|
27
|
+
const { component: Component } = data;
|
|
28
|
+
if (!Component) {
|
|
29
|
+
return ((0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: (0, clearSlash_1.clearSlash)(`${page.path}/${pathName}`), element: (0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: "/", replace: true }) }, `modals/${pathName}_${i}`));
|
|
30
|
+
}
|
|
31
|
+
return ((0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: (0, clearSlash_1.clearSlash)(`${page.path}/${pathName}`), element: (0, jsx_runtime_1.jsx)(Component, { onClose: handleClose }) }, `modals/${pathName}_${i}`));
|
|
32
|
+
}) }) }));
|
|
33
|
+
if (!ModalWrapper) {
|
|
34
|
+
return isOpen ? (0, jsx_runtime_1.jsx)(react_1.Fragment, { children: content }) : null;
|
|
35
|
+
}
|
|
36
|
+
return ((0, jsx_runtime_1.jsx)(ModalWrapper, { ref: modalRef, modalName: modalName, isOpen: isOpen, onClose: close, children: content }));
|
|
37
|
+
};
|
|
38
|
+
exports.ModalsContainer = ModalsContainer;
|
|
39
|
+
//# sourceMappingURL=ModalsContainer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalsContainer.js","sourceRoot":"","sources":["../../../src/PathRouter/Container/ModalsContainer.tsx"],"names":[],"mappings":";;;;AAAA,iCAAqE;AACrE,uDAA2D;AAE3D,iDAA8C;AAC9C,oDAAiD;AAkB1C,MAAM,eAAe,GAAkC,CAAC,EAC7D,KAAK,EACL,YAAY,EACZ,QAAQ,GAAG,IAAI,GAChB,EAAE,EAAE;IACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAA,iBAAO,GAAE,CAAC;IAClC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAA,cAAM,EAAyB,IAAI,CAAC,CAAC;IAEtD,MAAM,oBAAoB,GACxB,CAAC,CAAC,SAAS;QACX,KAAK,CAAC,IAAI,CACR,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CACnE,CAAC;IAEJ,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,IAAI,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACpC,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,EAAE,KAAK,CAAC,CAAC,CAAC;IAE1C,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAE5B,MAAM,cAAc,GAAG,IAAA,uBAAU,EAAC,GAAG,IAAI,CAAC,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;IAE/D,MAAM,WAAW,GAAG,GAAG,EAAE,CACvB,QAAQ,CAAC,OAAO,EAAE,wBAAwB;QACxC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,wBAAwB,EAAE;QAC7C,CAAC,CAAC,KAAK,EAAE,CAAC;IAEd,MAAM,OAAO,GAAG,CACd,uBAAC,gBAAQ,IAAC,QAAQ,EAAE,QAAQ,YAC1B,uBAAC,yBAAM,IAAC,QAAQ,EAAE,cAAc,YAC7B,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;gBACnC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;gBACtC,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,OAAO,CACL,uBAAC,wBAAK,IAEJ,IAAI,EAAE,IAAA,uBAAU,EAAC,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC,EAC5C,OAAO,EAAE,uBAAC,2BAAQ,IAAC,EAAE,EAAC,GAAG,EAAC,OAAO,SAAG,IAF/B,UAAU,QAAQ,IAAI,CAAC,EAAE,CAG9B,CACH,CAAC;gBACJ,CAAC;gBACD,OAAO,CACL,uBAAC,wBAAK,IAEJ,IAAI,EAAE,IAAA,uBAAU,EAAC,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC,EAC5C,OAAO,EAAE,uBAAC,SAAS,IAAC,OAAO,EAAE,WAAW,GAAI,IAFvC,UAAU,QAAQ,IAAI,CAAC,EAAE,CAG9B,CACH,CAAC;YACJ,CAAC,CAAC,GACK,GACA,CACZ,CAAC;IAEF,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,MAAM,CAAC,CAAC,CAAC,uBAAC,gBAAQ,cAAE,OAAO,GAAY,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC;IAED,OAAO,CACL,uBAAC,YAAY,IACX,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,KAAK,YACb,OAAO,GACK,CAChB,CAAC;AACJ,CAAC,CAAC;AArEW,QAAA,eAAe,mBAqE1B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { ModalWrapperComponent, RouterConfig } from "../types";
|
|
3
|
+
export interface PathRouterProps<C extends RouterConfig<any, any>> {
|
|
4
|
+
/** Pages + modals tree built with `setPage` / `setModal`. */
|
|
5
|
+
config: C;
|
|
6
|
+
/**
|
|
7
|
+
* Optional component used to wrap modal content (animated popup, etc.).
|
|
8
|
+
* The package will pass it `{ modalName, isOpen, onClose, children }`
|
|
9
|
+
* and read `handleCloseWithAnimation()` from its forwarded ref.
|
|
10
|
+
*/
|
|
11
|
+
ModalWrapper?: ModalWrapperComponent;
|
|
12
|
+
/** Suspense fallback shown while pages / modals are loading. */
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare const PathRouterContainer: <C extends RouterConfig<any, any>>({ config, ModalWrapper, fallback, }: PathRouterProps<C>) => React.JSX.Element;
|
|
16
|
+
//# sourceMappingURL=RouterContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouterContainer.d.ts","sourceRoot":"","sources":["../../../src/PathRouter/Container/RouterContainer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAOjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEpE,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC;IAC/D,6DAA6D;IAC7D,MAAM,EAAE,CAAC,CAAC;IACV;;;;OAIG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,qCAInE,eAAe,CAAC,CAAC,CAAC,sBAuCpB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PathRouterContainer = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_router_dom_1 = require("react-router-dom");
|
|
7
|
+
const usePath_1 = require("../Provider/usePath");
|
|
8
|
+
const createRoute_1 = require("../utils/createRoute");
|
|
9
|
+
const ModalsContainer_1 = require("./ModalsContainer");
|
|
10
|
+
const PathRouterContainer = ({ config, ModalWrapper, fallback = null, }) => {
|
|
11
|
+
const { modal } = (0, usePath_1.usePath)();
|
|
12
|
+
const { pages, modals } = (0, react_1.useMemo)(() => (0, createRoute_1.createRoute)(config), [config]);
|
|
13
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: fallback, children: (0, jsx_runtime_1.jsx)(react_router_dom_1.Routes, { children: pages.map(({ pathName, data }, i) => {
|
|
14
|
+
const { component: Component, redirect } = data;
|
|
15
|
+
const isRedirect = Boolean(!Component || redirect);
|
|
16
|
+
return ((0, jsx_runtime_1.jsx)(react_router_dom_1.Route, { path: pathName, element: !isRedirect && Component ? ((0, jsx_runtime_1.jsx)(Component, {})) : ((0, jsx_runtime_1.jsx)(react_router_dom_1.Navigate, { to: redirect || "/", replace: true })) }, `routes/${pathName}_${i}`));
|
|
17
|
+
}) }) }), modal.isOpen && ((0, jsx_runtime_1.jsx)(ModalsContainer_1.ModalsContainer, { paths: modals, ModalWrapper: ModalWrapper, fallback: fallback }))] }));
|
|
18
|
+
};
|
|
19
|
+
exports.PathRouterContainer = PathRouterContainer;
|
|
20
|
+
//# sourceMappingURL=RouterContainer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouterContainer.js","sourceRoot":"","sources":["../../../src/PathRouter/Container/RouterContainer.tsx"],"names":[],"mappings":";;;;AAAA,iCAAiE;AACjE,uDAAqE;AAErE,iDAA8C;AAC9C,sDAAmD;AAEnD,uDAAoD;AAgB7C,MAAM,mBAAmB,GAAG,CAAmC,EACpE,MAAM,EACN,YAAY,EACZ,QAAQ,GAAG,IAAI,GACI,EAAE,EAAE;IACvB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAA,iBAAO,GAAE,CAAC;IAE5B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAA,eAAO,EAAC,GAAG,EAAE,CAAC,IAAA,yBAAW,EAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEvE,OAAO,CACL,6DACE,uBAAC,gBAAQ,IAAC,QAAQ,EAAE,QAAQ,YAC1B,uBAAC,yBAAM,cACJ,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;wBACnC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;wBAChD,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC;wBAEnD,OAAO,CACL,uBAAC,wBAAK,IAEJ,IAAI,EAAE,QAAQ,EACd,OAAO,EACL,CAAC,UAAU,IAAI,SAAS,CAAC,CAAC,CAAC,CACzB,uBAAC,SAAS,KAAG,CACd,CAAC,CAAC,CAAC,CACF,uBAAC,2BAAQ,IAAC,EAAE,EAAE,QAAQ,IAAI,GAAG,EAAE,OAAO,SAAG,CAC1C,IAPE,UAAU,QAAQ,IAAI,CAAC,EAAE,CAS9B,CACH,CAAC;oBACJ,CAAC,CAAC,GACK,GACA,EAEV,KAAK,CAAC,MAAM,IAAI,CACf,uBAAC,iCAAe,IACd,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,QAAQ,GAClB,CACH,IACA,CACJ,CAAC;AACJ,CAAC,CAAC;AA3CW,QAAA,mBAAmB,uBA2C9B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/PathRouter/Container/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./RouterContainer"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/PathRouter/Container/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,oDAAkC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { type AnchorHTMLAttributes, type ReactNode, type Ref } from "react";
|
|
2
|
+
import type { NavigateOptions } from "react-router-dom";
|
|
3
|
+
import type { ModalNamesOf, PathNamesOf, RouterConfig } from "../types";
|
|
4
|
+
export interface NavLinkProps<C extends RouterConfig<any, any> = RouterConfig<any, any>> extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
5
|
+
/**
|
|
6
|
+
* Target page path. If omitted, the current page is preserved
|
|
7
|
+
* (useful when you only want to open a modal).
|
|
8
|
+
*/
|
|
9
|
+
to?: PathNamesOf<C>;
|
|
10
|
+
/** Modal to open on click. */
|
|
11
|
+
modal?: ModalNamesOf<C>;
|
|
12
|
+
/** Extra path segments appended after the modal name. */
|
|
13
|
+
modalBreadCrumbs?: string[];
|
|
14
|
+
/** Replace history entry instead of pushing a new one. */
|
|
15
|
+
replace?: boolean;
|
|
16
|
+
/** Extra options forwarded to the underlying `navigate`. */
|
|
17
|
+
navigateOptions?: NavigateOptions;
|
|
18
|
+
/** Class applied when this link matches the current location. */
|
|
19
|
+
activeClassName?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Router-aware anchor.
|
|
24
|
+
*
|
|
25
|
+
* Renders a real `<a href>` (so right-click / "open in new tab" / SSR work)
|
|
26
|
+
* and intercepts the primary-button click to call `page.navigate` /
|
|
27
|
+
* open a modal through the `PathRouter` context.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <NavLink<typeof config> to="add">Add item</NavLink>
|
|
31
|
+
* <NavLink<typeof config> modal="confirm">Open confirm</NavLink>
|
|
32
|
+
* <NavLink<typeof config> to="users" modal="confirm" modalBreadCrumbs={["step-2"]}>
|
|
33
|
+
* Go to users + open confirm at step 2
|
|
34
|
+
* </NavLink>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const NavLink: <C extends RouterConfig<any, any> = RouterConfig<any, any>>(props: NavLinkProps<C> & {
|
|
38
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
39
|
+
}) => React.ReactElement | null;
|
|
40
|
+
//# sourceMappingURL=NavLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NavLink.d.ts","sourceRoot":"","sources":["../../../src/PathRouter/NavLink/NavLink.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAEZ,KAAK,oBAAoB,EAEzB,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIxD,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,UAAU,CAAC;AAKlB,MAAM,WAAW,YAAY,CAC3B,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CACzD,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC7D;;;OAGG;IACH,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACpB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACxB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAuFD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,OAAO,EAAmB,CACrC,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,EAEzD,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAA;CAAE,KACtD,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
|