next-modal-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +592 -0
- package/assets/next-modal-router-header.png +0 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +547 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +64 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +166 -0
- package/docs/cli.md +16 -0
- package/docs/concepts.md +17 -0
- package/docs/configuration.md +24 -0
- package/docs/limitations.md +9 -0
- package/docs/nested-overlays.md +13 -0
- package/docs/troubleshooting.md +29 -0
- package/docs/ui-libraries.md +7 -0
- package/package.json +104 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. The project follows semantic versioning.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-08-31
|
|
6
|
+
|
|
7
|
+
- Added the headless overlay navigation provider, link, hooks, and query helpers.
|
|
8
|
+
- Added safe-close provenance and lightweight focus restoration.
|
|
9
|
+
- Added route-segment parsing, interception calculation, and filesystem discovery.
|
|
10
|
+
- Added `init`, `add`, `check`, `doctor`, and `list` CLI commands.
|
|
11
|
+
- Added structured diagnostics, JSON/CI output, dry-run generation, and monorepo targeting.
|
|
12
|
+
- Added the Next.js 16 basic example, documentation, tests, and release metadata.
|
|
13
|
+
- Verified and declared compatibility with Next.js 14.2+, React 18.2+, and Node.js 18.17+.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Ranjbar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# next-modal-router
|
|
2
|
+
|
|
3
|
+
> URL-native modals, drawers and overlays for Next.js App Router — without the routing headache.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[GitHub Repository](https://github.com/RanjbarAli/next-modal-router) · [npm package](https://www.npmjs.com/package/next-modal-router) *(first release not published yet)*
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/next-modal-router)
|
|
10
|
+
[](https://www.npmjs.com/package/next-modal-router)
|
|
11
|
+
[](https://github.com/RanjbarAli/next-modal-router/actions/workflows/ci.yml)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
|
|
14
|
+
`next-modal-router` supplies the small runtime and the route tooling needed to build overlays with Next.js Parallel Routes and Intercepting Routes. It does not replace the App Router and it does not render a dialog design system.
|
|
15
|
+
|
|
16
|
+
## Why?
|
|
17
|
+
|
|
18
|
+
The native model is excellent: navigate softly from `/products` to `/products/42`, show the product over the list, retain the real URL, and render a full page when that URL is loaded directly. The awkward part is maintaining infrastructure such as:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
app/@modal/(.)products/[id]/page.tsx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
along with `default.tsx`, the full-page counterpart, the correct interception depth, safe closing, and nested browser history. This package turns that work into:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx next-modal-router init --yes
|
|
28
|
+
npx next-modal-router add product \
|
|
29
|
+
--route "/products/[id]" \
|
|
30
|
+
--source "/products" \
|
|
31
|
+
--type modal \
|
|
32
|
+
--fallback "/products"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- URL-native, refresh-safe routes built on the App Router
|
|
38
|
+
- Safe close behavior with an explicit fallback for direct entries
|
|
39
|
+
- Typed `OverlayLink`, navigation hook, state hook, and query helpers
|
|
40
|
+
- Intercepting-route generation with route-segment-aware calculations
|
|
41
|
+
- Config-driven validation and zero-config filesystem discovery
|
|
42
|
+
- Logical nested depth backed by URL navigation—not a detached SPA stack
|
|
43
|
+
- Headless modal, drawer, sheet, panel, or custom semantics
|
|
44
|
+
- CI-friendly JSON output, deterministic exit codes, dry runs, and `--cwd`
|
|
45
|
+
- Small ESM runtime with no UI dependency
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add next-modal-router
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install next-modal-router
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Next.js 14.2–16 and React 18.2–19 are peer dependencies.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Initialize the config and default slot:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx next-modal-router init --yes
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Generate a product overlay:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx next-modal-router add product \
|
|
71
|
+
--route "/products/[id]" \
|
|
72
|
+
--source "/products" \
|
|
73
|
+
--slot modal \
|
|
74
|
+
--type modal \
|
|
75
|
+
--fallback "/products"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The command reports every write and creates a compilable starting point:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
app/
|
|
82
|
+
├── @modal/
|
|
83
|
+
│ ├── default.tsx
|
|
84
|
+
│ └── (.)products/
|
|
85
|
+
│ └── [id]/
|
|
86
|
+
│ └── page.tsx
|
|
87
|
+
└── products/
|
|
88
|
+
└── [id]/
|
|
89
|
+
└── page.tsx
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Wire the slot and provider into the layout that owns `@modal`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { Suspense, type ReactNode } from "react"
|
|
96
|
+
import { OverlayRouterProvider } from "next-modal-router"
|
|
97
|
+
|
|
98
|
+
export default function Layout({
|
|
99
|
+
children,
|
|
100
|
+
modal,
|
|
101
|
+
}: {
|
|
102
|
+
children: ReactNode
|
|
103
|
+
modal: ReactNode
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<OverlayRouterProvider>
|
|
107
|
+
{children}
|
|
108
|
+
<Suspense fallback={null}>{modal}</Suspense>
|
|
109
|
+
</OverlayRouterProvider>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Link to the real route:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { OverlayLink } from "next-modal-router"
|
|
118
|
+
|
|
119
|
+
<OverlayLink href="/products/42" fallback="/products" scroll={false}>
|
|
120
|
+
View product
|
|
121
|
+
</OverlayLink>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use your preferred dialog UI in the intercepted page and close it safely:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
"use client"
|
|
128
|
+
|
|
129
|
+
import { useOverlayRouter } from "next-modal-router"
|
|
130
|
+
|
|
131
|
+
export default function ProductModal() {
|
|
132
|
+
const overlay = useOverlayRouter()
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<section role="dialog" aria-modal="true" aria-labelledby="product-title">
|
|
136
|
+
<h2 id="product-title">Product 42</h2>
|
|
137
|
+
<button onClick={() => overlay.close()}>Close</button>
|
|
138
|
+
</section>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A click from `/products` shows an overlay at `/products/42`. Loading or refreshing `/products/42` renders `app/products/[id]/page.tsx` as a normal page.
|
|
144
|
+
|
|
145
|
+
## URL-native overlays
|
|
146
|
+
|
|
147
|
+
An overlay is always a route. `OverlayLink` delegates navigation to Next.js `Link`; `useOverlayRouter` delegates to `useRouter`; and the generated folders use the official `@slot` and interception conventions. Bookmarks, sharing, reloads, and server rendering therefore retain native behavior.
|
|
148
|
+
|
|
149
|
+
Use an overlay when the destination deserves a URL. For transient confirmations or menus with no meaningful destination, a regular local-state component is a better fit.
|
|
150
|
+
|
|
151
|
+
## Soft navigation versus direct navigation
|
|
152
|
+
|
|
153
|
+
| Entry | Next.js result | Package state |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| Click `/products` → `/products/42` | Intercepted slot route | depth `1`, safe back recorded |
|
|
156
|
+
| Paste or refresh `/products/42` | Full-page route | depth `0`, close uses fallback |
|
|
157
|
+
| Back/forward | Native history traversal | App Router decides rendered route |
|
|
158
|
+
|
|
159
|
+
Interception is a Next.js soft-navigation behavior. This package deliberately does not force an intercepted UI on hard navigation.
|
|
160
|
+
|
|
161
|
+
## Safe close behavior
|
|
162
|
+
|
|
163
|
+
Calling `router.back()` blindly can send a direct visitor back to another website. The provider records only overlay navigation initiated by `OverlayLink` or `overlay.open()` during the current application lifetime.
|
|
164
|
+
|
|
165
|
+
- A recorded current overlay closes with `router.back()`.
|
|
166
|
+
- A direct load, missing marker, or uncertain state closes with `router.replace(fallback)`.
|
|
167
|
+
- If no fallback exists, the conservative default is `/`.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<OverlayLink href="/products/42" fallback="/products">
|
|
171
|
+
View product
|
|
172
|
+
</OverlayLink>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The browser does not expose arbitrary history entries, so this is intentionally a safe heuristic rather than a claim of perfect history knowledge. Configure `closeFallback` and pass `fallback` when the direct-entry close destination matters.
|
|
176
|
+
|
|
177
|
+
## Modal routing
|
|
178
|
+
|
|
179
|
+
Set `type: "modal"` for documentation and tooling semantics. The intercepted page controls the actual markup:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
product: {
|
|
183
|
+
route: "/products/[id]",
|
|
184
|
+
source: "/products",
|
|
185
|
+
type: "modal",
|
|
186
|
+
slot: "modal",
|
|
187
|
+
closeFallback: "/products",
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The type never injects styles, portals, focus traps, or animation.
|
|
192
|
+
|
|
193
|
+
## Drawer, sheet, and panel routing
|
|
194
|
+
|
|
195
|
+
Drawers and sheets use exactly the same routing mechanism. A separate slot is useful when two overlay surfaces should be independent:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
notifications: {
|
|
199
|
+
route: "/notifications",
|
|
200
|
+
source: "/dashboard",
|
|
201
|
+
type: "drawer",
|
|
202
|
+
slot: "panel",
|
|
203
|
+
closeFallback: "/dashboard",
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Render the intercepted `@panel/(.)notifications/page.tsx` with your own sheet or drawer component.
|
|
208
|
+
|
|
209
|
+
## Nested overlays
|
|
210
|
+
|
|
211
|
+
Each call to `overlay.open()` or click on `OverlayLink` records one logical level:
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
/products → /products/42 → /products/42/reviews
|
|
215
|
+
depth 0 depth 1 depth 2
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
<OverlayLink href={`/products/${id}/reviews`} fallback={`/products/${id}`}>
|
|
220
|
+
Reviews
|
|
221
|
+
</OverlayLink>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`overlay.close()` traverses one native history entry when that level was recorded safely. The actual visual composition still follows Next.js slot rendering: use another parallel slot or render the parent shell in the nested intercepted route when both layers must remain visible.
|
|
225
|
+
|
|
226
|
+
See [Nested overlays](./docs/nested-overlays.md).
|
|
227
|
+
|
|
228
|
+
## Browser back and forward
|
|
229
|
+
|
|
230
|
+
`overlay.back()` and `overlay.forward()` are direct App Router operations. Browser buttons work normally because the URL and Next.js history remain authoritative. The package does not patch `pushState`, attach a global popstate router, or keep a competing route store.
|
|
231
|
+
|
|
232
|
+
## Query parameter helpers
|
|
233
|
+
|
|
234
|
+
Update a subset of parameters without discarding the rest:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const overlay = useOverlayRouter()
|
|
238
|
+
|
|
239
|
+
overlay.setSearchParams({ tab: "reviews" })
|
|
240
|
+
// /products/42?tab=reviews
|
|
241
|
+
|
|
242
|
+
overlay.setSearchParams({ tab: "details", sort: "newest" })
|
|
243
|
+
// /products/42?sort=newest&tab=details
|
|
244
|
+
|
|
245
|
+
overlay.setSearchParams({ tab: null })
|
|
246
|
+
// removes tab and preserves sort
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The standalone pure helper is useful outside React:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { updateSearchParams } from "next-modal-router"
|
|
253
|
+
|
|
254
|
+
const next = updateSearchParams("tab=details&sort=newest", { tab: "reviews" })
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Updates use `router.replace()` and preserve Next.js navigation semantics.
|
|
258
|
+
|
|
259
|
+
## Scroll behavior
|
|
260
|
+
|
|
261
|
+
`OverlayLink` defaults `scroll` to `false`, which is normally appropriate for preserving the underlying page position. Override it with the standard Next.js prop when the destination should scroll:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
<OverlayLink href="/products/42" scroll>
|
|
265
|
+
View product
|
|
266
|
+
</OverlayLink>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The package adds no scroll restoration hacks.
|
|
270
|
+
|
|
271
|
+
## Focus restoration
|
|
272
|
+
|
|
273
|
+
The provider records the focused trigger for internal overlay navigation and focuses it after close when it remains connected. This lightweight behavior is enabled by default and interoperates with dialog libraries:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<OverlayRouterProvider restoreFocus={false}>
|
|
277
|
+
{children}
|
|
278
|
+
{modal}
|
|
279
|
+
</OverlayRouterProvider>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Disable it when your UI library owns focus restoration. Focus trapping and initial modal focus remain the responsibility of that accessible dialog implementation.
|
|
283
|
+
|
|
284
|
+
## Route generation
|
|
285
|
+
|
|
286
|
+
`add` models URL segments separately from filesystem segments, ignores route groups for interception depth, validates route inputs against traversal, and emits atomic writes. Existing full-page routes and slot defaults are preserved; conflicting intercepted pages require `--force`.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npx next-modal-router add reviews \
|
|
290
|
+
--route "/products/[id]/reviews" \
|
|
291
|
+
--source "/products/[id]" \
|
|
292
|
+
--type drawer \
|
|
293
|
+
--slot modal \
|
|
294
|
+
--fallback "/products/[id]" \
|
|
295
|
+
--dry-run
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Route validation
|
|
299
|
+
|
|
300
|
+
`check` verifies slot defaults, configured interceptors, full-page counterparts, and fallback pages. Diagnostics use stable codes:
|
|
301
|
+
|
|
302
|
+
| Code | Meaning |
|
|
303
|
+
| --- | --- |
|
|
304
|
+
| `NMR001` | Missing slot `default` route |
|
|
305
|
+
| `NMR002` | Missing configured interceptor |
|
|
306
|
+
| `NMR003` | Incorrect interception path |
|
|
307
|
+
| `NMR004` | Missing full-page counterpart |
|
|
308
|
+
| `NMR005` | Missing close fallback page |
|
|
309
|
+
| `NMR006` | Discovered overlay absent from config |
|
|
310
|
+
|
|
311
|
+
## Zero-config discovery
|
|
312
|
+
|
|
313
|
+
`check` and `list` still scan existing `@slot` directories when no config exists. For example, `app/@modal/(.)photos/[id]/page.tsx` appears as a discovered overlay. Add a config when you want source, fallback, and expected-target validation.
|
|
314
|
+
|
|
315
|
+
## Config-driven workflows
|
|
316
|
+
|
|
317
|
+
Configuration is the explicit contract used by generation and CI. It does not ship to the browser and is not required by the runtime.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { defineConfig } from "next-modal-router/config"
|
|
321
|
+
|
|
322
|
+
export default defineConfig({
|
|
323
|
+
defaultSlot: "modal",
|
|
324
|
+
overlays: {
|
|
325
|
+
product: {
|
|
326
|
+
route: "/products/[id]",
|
|
327
|
+
source: "/products",
|
|
328
|
+
type: "modal",
|
|
329
|
+
slot: "modal",
|
|
330
|
+
closeFallback: "/products",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
See [Configuration](./docs/configuration.md).
|
|
337
|
+
|
|
338
|
+
## CLI usage
|
|
339
|
+
|
|
340
|
+
### `init`
|
|
341
|
+
|
|
342
|
+
Detects a Next.js App Router project and TypeScript, then creates the config and default slot fallback.
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
next-modal-router init [--slot modal] [--yes] [--dry-run] [--force]
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
CREATE next-modal-router.config.ts
|
|
350
|
+
CREATE app/@modal/default.tsx
|
|
351
|
+
|
|
352
|
+
Initialized next-modal-router in /project.
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Exit `0` means success; misuse or a protected-file conflict exits `2`.
|
|
356
|
+
|
|
357
|
+
### `add`
|
|
358
|
+
|
|
359
|
+
Interactively prompts for missing values on a terminal, or accepts a complete scriptable definition:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
next-modal-router add product --route "/products/[id]" --source "/products" --type modal --slot modal --fallback "/products"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
It generates the slot default if missing, intercepted page, full page if missing, and synchronized config. Exit `0` means every planned write completed.
|
|
366
|
+
|
|
367
|
+
### `check`
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
next-modal-router check --verbose
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
next-modal-router
|
|
375
|
+
|
|
376
|
+
Checking 1 overlay...
|
|
377
|
+
|
|
378
|
+
✓ Everything looks good.
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Validation errors exit `1`; invocation/configuration failures exit `2`.
|
|
382
|
+
|
|
383
|
+
### `doctor`
|
|
384
|
+
|
|
385
|
+
Reports Node, Next.js, React, App Router, TypeScript, config, and validation health:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
next-modal-router doctor --cwd apps/web
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Use this first when the generator cannot locate the intended application.
|
|
392
|
+
|
|
393
|
+
### `list`
|
|
394
|
+
|
|
395
|
+
Lists both configured and discovered routes:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
next-modal-router list
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```text
|
|
402
|
+
NAME ROUTE TYPE SLOT ORIGIN
|
|
403
|
+
product /products/[id] modal modal config
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Global options
|
|
407
|
+
|
|
408
|
+
| Option | Commands | Effect |
|
|
409
|
+
| --- | --- | --- |
|
|
410
|
+
| `--cwd <path>` | all | Run against an app elsewhere in a monorepo |
|
|
411
|
+
| `--format json` | check, doctor, list | Emit JSON without ANSI text |
|
|
412
|
+
| `--dry-run` | init, add | Print writes without changing files |
|
|
413
|
+
| `--force` | init, add | Replace files owned by the requested operation |
|
|
414
|
+
| `--ci` | all | Disable prompting and keep deterministic output |
|
|
415
|
+
| `--verbose` | check | Include diagnostic filesystem paths |
|
|
416
|
+
| `--yes` | init, add | Accept non-interactive defaults |
|
|
417
|
+
|
|
418
|
+
## CI usage
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
next-modal-router check --ci --format json > overlay-report.json
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Exit codes are stable: `0` success, `1` route validation errors, `2` CLI/configuration misuse. Warnings do not fail `check`.
|
|
425
|
+
|
|
426
|
+
## Monorepo support
|
|
427
|
+
|
|
428
|
+
Run the CLI from the actual application directory or target it explicitly:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
next-modal-router doctor --cwd ./apps/storefront
|
|
432
|
+
next-modal-router check --cwd ./apps/storefront --ci
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Discovery stops at the located package containing `app/` or `src/app/`; it does not scan unrelated workspaces.
|
|
436
|
+
|
|
437
|
+
## UI-library interoperability
|
|
438
|
+
|
|
439
|
+
The route owns visibility; the UI library owns accessibility and presentation.
|
|
440
|
+
|
|
441
|
+
### shadcn/ui / Radix Dialog
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
"use client"
|
|
445
|
+
|
|
446
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
|
447
|
+
import { useOverlayRouter } from "next-modal-router"
|
|
448
|
+
|
|
449
|
+
export default function ProductOverlay() {
|
|
450
|
+
const overlay = useOverlayRouter()
|
|
451
|
+
return (
|
|
452
|
+
<Dialog open onOpenChange={(open) => !open && overlay.close()}>
|
|
453
|
+
<DialogContent>
|
|
454
|
+
<DialogTitle>Product</DialogTitle>
|
|
455
|
+
</DialogContent>
|
|
456
|
+
</Dialog>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
For Radix directly, use `Dialog.Root open` and the same `onOpenChange` rule. Disable provider focus restoration if Radix should restore focus itself.
|
|
462
|
+
|
|
463
|
+
### Headless UI and sheets
|
|
464
|
+
|
|
465
|
+
Use `Dialog open` with `onClose={overlay.close}` in Headless UI. Sheet libraries built on Radix use the same controlled-open pattern. Do not toggle local `open` state after close; navigation unmounts the intercepted route.
|
|
466
|
+
|
|
467
|
+
More examples: [UI libraries](./docs/ui-libraries.md).
|
|
468
|
+
|
|
469
|
+
## Configuration reference
|
|
470
|
+
|
|
471
|
+
`defineConfig(config)` returns the same object while preserving literal overlay names and values.
|
|
472
|
+
|
|
473
|
+
| Property | Type | Required | Default | Purpose |
|
|
474
|
+
| --- | --- | --- | --- | --- |
|
|
475
|
+
| `defaultSlot` | `string` | no | `"modal"` in CLI | Slot used when an overlay omits `slot` |
|
|
476
|
+
| `overlays` | `Record<string, OverlayDefinition>` | yes | — | Named overlay definitions |
|
|
477
|
+
| `route` | absolute route string | yes | — | Shareable destination URL pattern |
|
|
478
|
+
| `source` | absolute route string | yes | — | Page from which soft navigation is intended |
|
|
479
|
+
| `type` | `modal \| drawer \| sheet \| panel \| custom` | yes | — | Semantic tooling label only |
|
|
480
|
+
| `slot` | `string` | no | `defaultSlot` | Parallel route slot, without `@` |
|
|
481
|
+
| `closeFallback` | absolute route string | yes | — | Safe close destination on direct/uncertain entry |
|
|
482
|
+
|
|
483
|
+
Route values use URL syntax, not filesystem traversal. Route groups can appear and do not contribute a URL segment. JavaScript, MJS, MTS, and TypeScript configs are loadable; TypeScript is generated by default.
|
|
484
|
+
|
|
485
|
+
## TypeScript API
|
|
486
|
+
|
|
487
|
+
### `OverlayRouterProvider`
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
function OverlayRouterProvider(props: {
|
|
491
|
+
children: ReactNode
|
|
492
|
+
defaultFallback?: string
|
|
493
|
+
restoreFocus?: boolean
|
|
494
|
+
}): JSX.Element
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Provides minimal in-memory navigation provenance. Place it around the layout children and relevant parallel slots.
|
|
498
|
+
|
|
499
|
+
### `OverlayLink`
|
|
500
|
+
|
|
501
|
+
Accepts Next.js `LinkProps`, anchor attributes, and `fallback?: string`. It defaults `scroll` to `false`, forwards its ref, respects modified clicks, and records only navigations that Next.js will handle.
|
|
502
|
+
|
|
503
|
+
### `useOverlayRouter`
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
interface OverlayRouter {
|
|
509
|
+
open(href: string, options?: { fallback?: string; scroll?: boolean }): void
|
|
510
|
+
replace(href: string, options?: { fallback?: string; scroll?: boolean }): void
|
|
511
|
+
close(fallback?: string): void
|
|
512
|
+
back(): void
|
|
513
|
+
forward(): void
|
|
514
|
+
refresh(): void
|
|
515
|
+
setSearchParams(updates: SearchParamUpdates, options?: { scroll?: boolean }): void
|
|
516
|
+
isOpen: boolean
|
|
517
|
+
isOverlayNavigation: boolean
|
|
518
|
+
pathname: string
|
|
519
|
+
searchParams: ReadonlyURLSearchParams
|
|
520
|
+
previousPathname?: string
|
|
521
|
+
depth: number
|
|
522
|
+
fallback?: string
|
|
523
|
+
canGoBackSafely: boolean
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### `useOverlayState`
|
|
528
|
+
|
|
529
|
+
Returns the read-only navigation portion without router methods. Use it for diagnostics, breadcrumbs, or choosing nested UI presentation.
|
|
530
|
+
|
|
531
|
+
### `updateSearchParams` and `withSearchParams`
|
|
532
|
+
|
|
533
|
+
Pure utilities accepting string, number, boolean, null, undefined, or arrays. `null`/`undefined` remove a key; all unrelated keys remain.
|
|
534
|
+
|
|
535
|
+
### Public types
|
|
536
|
+
|
|
537
|
+
The root exports `OverlayConfig`, `OverlayDefinition`, `OverlayType`, `OverlayNavigationOptions`, `OverlayRouter`, `OverlayState`, `OverlayLinkProps`, `OverlayRouterProviderProps`, `SearchParamUpdates`, `SearchParamValue`, `RouteSegment`, `RouteSegmentKind`, `ValidationIssue`, and `ValidationResult`. Config helpers live at `next-modal-router/config`.
|
|
538
|
+
|
|
539
|
+
## How it works
|
|
540
|
+
|
|
541
|
+
Parallel Routes let a layout render named slots beside `children`. Intercepting Routes tell Next.js that a soft navigation should render a target inside a slot. On a hard request the ordinary target page wins. The CLI creates and checks this filesystem contract; the runtime adds only navigation provenance, conservative closing, and query convenience.
|
|
542
|
+
|
|
543
|
+
No custom router, global history patch, local storage, or route manifest is required at runtime.
|
|
544
|
+
|
|
545
|
+
## Troubleshooting
|
|
546
|
+
|
|
547
|
+
- **Modal remains visible after navigation:** ensure every slot has `default.tsx` returning `null`, and use navigation rather than only changing local dialog state.
|
|
548
|
+
- **Overlay appears on refresh:** confirm a full `app/products/[id]/page.tsx` exists outside `@modal`; an always-open dialog in a shared layout is not an intercepted route.
|
|
549
|
+
- **Overlay does not appear on click:** use `Link`, `OverlayLink`, or `router.push`; typing a URL is intentionally a hard navigation.
|
|
550
|
+
- **Back leaves the website:** use `overlay.close()` with a fallback, not unconditional `router.back()`.
|
|
551
|
+
- **Parallel slot produces 404:** pass the slot prop through the layout and add its `default.tsx`.
|
|
552
|
+
- **Incorrect intercept depth:** run `next-modal-router check --verbose`; matchers count route segments, not `@slots` or route groups.
|
|
553
|
+
|
|
554
|
+
See [Troubleshooting](./docs/troubleshooting.md) for detailed diagnosis.
|
|
555
|
+
|
|
556
|
+
## Limitations
|
|
557
|
+
|
|
558
|
+
- Native App Router restrictions and interception behavior still apply.
|
|
559
|
+
- Browser history cannot be inspected arbitrarily; safe close relies on navigation recorded during the current provider lifetime plus an explicit fallback.
|
|
560
|
+
- Opening overlays through a raw custom `router.push` call cannot be recorded; use `overlay.open` or `OverlayLink`.
|
|
561
|
+
- The generator places configured slots at the app root. Existing nested slots are discovered and can be maintained manually.
|
|
562
|
+
- Rewriting an existing config during `add` normalizes it; comments and custom computed configuration are not preserved. Use `--dry-run` and maintain unusual configs manually.
|
|
563
|
+
- Visual accessibility beyond lightweight focus restoration belongs to the selected dialog library.
|
|
564
|
+
|
|
565
|
+
Full details: [Limitations](./docs/limitations.md).
|
|
566
|
+
|
|
567
|
+
## Compatibility
|
|
568
|
+
|
|
569
|
+
| Dependency | Policy | Repository verification |
|
|
570
|
+
| --- | --- | --- |
|
|
571
|
+
| Node.js | `>=18.17` | compatibility CI: Node 18; primary CI: Node 20 and 22 |
|
|
572
|
+
| Next.js | `>=14.2 <17` | compatibility CI: 14.2 and 15.5; example: 16.3.3 |
|
|
573
|
+
| React / React DOM | `>=18.2 <20` | compatibility CI: React 18; example: React 19.2.8 |
|
|
574
|
+
| TypeScript | modern strict projects | development on 5.9 |
|
|
575
|
+
|
|
576
|
+
## Migration and versioning
|
|
577
|
+
|
|
578
|
+
The project follows semantic versioning. Before `1.0`, minor versions may intentionally refine generated structure or public APIs and will document changes in [CHANGELOG.md](./CHANGELOG.md). Generated files remain application-owned; upgrading never rewrites them automatically.
|
|
579
|
+
|
|
580
|
+
## Contributing
|
|
581
|
+
|
|
582
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, architecture, test expectations, and pull-request guidance. Please follow the [Code of Conduct](./CODE_OF_CONDUCT.md). Security reports belong in GitHub private vulnerability reporting as described in [SECURITY.md](./SECURITY.md).
|
|
583
|
+
|
|
584
|
+
## License
|
|
585
|
+
|
|
586
|
+
MIT © Ali Ranjbar. See [LICENSE](./LICENSE).
|
|
587
|
+
|
|
588
|
+
## Project links
|
|
589
|
+
|
|
590
|
+
- GitHub: https://github.com/RanjbarAli/next-modal-router
|
|
591
|
+
- Issues: https://github.com/RanjbarAli/next-modal-router/issues
|
|
592
|
+
- npm: https://www.npmjs.com/package/next-modal-router *(available after the first publication)*
|
|
Binary file
|
package/dist/cli.d.ts
ADDED