@okyrychenko-dev/react-action-guard-router 0.2.4 → 1.0.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 CHANGED
@@ -1,14 +1,21 @@
1
1
  # @okyrychenko-dev/react-action-guard-router
2
2
 
3
+ See the [versioned capability matrix](https://github.com/okyrychenko-dev/react-action-guard/blob/main/CAPABILITIES.md) for evaluated peers, reproducible checks, evidence kinds, and limitations.
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/@okyrychenko-dev/react-action-guard-router.svg)](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard-router)
4
6
  [![npm downloads](https://img.shields.io/npm/dm/@okyrychenko-dev/react-action-guard-router.svg)](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard-router)
5
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
8
 
7
9
  > Router integration for React Action Guard - navigation blocking for React Router, Remix, TanStack Router, and Next.js
8
10
 
11
+ Start with the [core-only coordination example](https://github.com/okyrychenko-dev/react-action-guard/tree/main/examples/core-coordination)
12
+ to see one operation affect two independent controls. Add this package when you need its
13
+ integration. [Canonical documentation and local instructions](https://github.com/okyrychenko-dev/react-action-guard/tree/main/packages/docs)
14
+ provide the next learning step.
15
+
9
16
  ## Features
10
17
 
11
- - 🛣️ **Multi-Router Support** - React Router v6+, Remix, TanStack Router, Next.js Pages Router, and best-effort App Router support
18
+ - 🛣️ **Multi-Router Support** - React Router v6.19+, Remix, TanStack Router, limited Next.js Pages Router interception, and App Router unload-only protection
12
19
  - 🎯 **Scope-Based Blocking** - Synchronize navigation blocking with UI blocking scopes
13
20
  - 🚦 **Condition-Based Blocking** - Block based on boolean conditions or functions
14
21
  - 💬 **Custom Dialog Support** - `useDialogState` helper for async confirmation dialogs
@@ -30,12 +37,12 @@ pnpm add @okyrychenko-dev/react-action-guard-router @okyrychenko-dev/react-actio
30
37
 
31
38
  This package requires the following peer dependencies:
32
39
 
33
- - [@okyrychenko-dev/react-action-guard](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard) ^1.0.1 - The core UI blocking library
40
+ - [@okyrychenko-dev/react-action-guard](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard) ^2.0.0 - The core UI blocking library
34
41
  - [React](https://react.dev/) ^18.0.0 || ^19.0.0
35
42
  - One of:
36
- - [react-router-dom](https://reactrouter.com/) ^6.0.0 - For React Router or Remix
37
- - [@tanstack/react-router](https://tanstack.com/router) ^1.0.0 - For TanStack Router
38
- - [next](https://nextjs.org/) ^13.4.0 - For Next.js Pages Router and best-effort App Router support
43
+ - [react-router-dom](https://reactrouter.com/) ^6.19.0 - For React Router or Remix
44
+ - [@tanstack/react-router](https://tanstack.com/router) ^1.170.41 - For TanStack Router
45
+ - [next](https://nextjs.org/) ^13.4.0 - For limited Pages Router interception and App Router unload-only protection
39
46
  - [Zustand](https://zustand-demo.pmnd.rs/) - State management (peer dependency of react-action-guard)
40
47
 
41
48
  ## Quick Start
@@ -45,22 +52,26 @@ This package requires the following peer dependencies:
45
52
  The most powerful pattern - synchronize navigation blocking with UI blocking:
46
53
 
47
54
  ```tsx
48
- import { useBlocker } from '@okyrychenko-dev/react-action-guard';
49
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';
55
+ import { useActionBlocker } from "@okyrychenko-dev/react-action-guard";
56
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/react-router";
50
57
 
51
58
  function PaymentForm() {
52
59
  const [isProcessing, setIsProcessing] = useState(false);
53
60
 
54
61
  // Block UI during payment processing
55
- useBlocker('payment-processing', {
56
- scope: 'checkout',
57
- reason: 'Processing payment...',
58
- }, isProcessing);
62
+ useActionBlocker(
63
+ "payment-processing",
64
+ {
65
+ scope: "checkout",
66
+ reason: "Processing payment...",
67
+ },
68
+ isProcessing
69
+ );
59
70
 
60
71
  // Block navigation when the same scope is active
61
72
  useNavigationBlocker({
62
- scope: 'checkout',
63
- message: 'Payment is processing. Please wait.',
73
+ scope: "checkout",
74
+ message: "Payment is processing. Please wait.",
64
75
  });
65
76
 
66
77
  // When isProcessing = true, both UI and navigation are blocked
@@ -72,18 +83,27 @@ function PaymentForm() {
72
83
  For basic usage without react-action-guard scopes:
73
84
 
74
85
  ```tsx
75
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';
86
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/react-router";
76
87
 
77
88
  function EditForm() {
78
89
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
79
90
 
80
91
  useNavigationBlocker({
81
92
  when: hasUnsavedChanges,
82
- message: 'You have unsaved changes. Leave anyway?',
93
+ message: "You have unsaved changes. Leave anyway?",
83
94
  });
84
95
  }
85
96
  ```
86
97
 
98
+ <details>
99
+ <summary>Detailed reference and integration patterns</summary>
100
+
101
+ ## Canonical reference
102
+
103
+ Read the [public reference](https://github.com/okyrychenko-dev/react-action-guard/blob/main/packages/docs/src/packages/react-action-guard-router/reference.md)
104
+ and [integration guide](https://github.com/okyrychenko-dev/react-action-guard/blob/main/packages/docs/src/packages/react-action-guard-router/index.md).
105
+ The expandable material below remains available for existing links.
106
+
87
107
  ## API Reference
88
108
 
89
109
  ### Hooks
@@ -109,7 +129,8 @@ Blocks navigation in router applications based on conditions or scope state.
109
129
  - `isIntercepting?: boolean` - Active interception state when the router can expose it
110
130
 
111
131
  **Available in:**
112
- - `@okyrychenko-dev/react-action-guard-router/react-router` - React Router v6+ & Remix
132
+
133
+ - `@okyrychenko-dev/react-action-guard-router/react-router` - React Router v6.19+ & Remix
113
134
  - `@okyrychenko-dev/react-action-guard-router/tanstack-router` - TanStack Router
114
135
  - `@okyrychenko-dev/react-action-guard-router/nextjs` - Next.js Pages & App Router
115
136
 
@@ -118,28 +139,28 @@ Blocks navigation in router applications based on conditions or scope state.
118
139
  ```tsx
119
140
  // Scope-based blocking
120
141
  useNavigationBlocker({
121
- scope: 'form',
122
- message: 'You have unsaved changes',
142
+ scope: "form",
143
+ message: "You have unsaved changes",
123
144
  });
124
145
 
125
146
  // Condition-based blocking
126
147
  useNavigationBlocker({
127
148
  when: hasChanges,
128
- message: 'Discard changes?',
129
- onBlock: () => console.log('Navigation blocked'),
130
- onAllow: () => console.log('Navigation allowed'),
149
+ message: "Discard changes?",
150
+ onBlock: () => console.log("Navigation blocked"),
151
+ onAllow: () => console.log("Navigation allowed"),
131
152
  });
132
153
 
133
154
  // Multiple scopes
134
155
  useNavigationBlocker({
135
- scope: ['form', 'validation'],
136
- message: 'Form is being validated',
156
+ scope: ["form", "validation"],
157
+ message: "Form is being validated",
137
158
  });
138
159
 
139
160
  // Function condition
140
161
  useNavigationBlocker({
141
162
  when: () => formIsDirty() || hasUnsavedData(),
142
- message: 'You have unsaved work',
163
+ message: "You have unsaved work",
143
164
  });
144
165
  ```
145
166
 
@@ -150,18 +171,35 @@ Helper hook for managing custom confirmation dialogs.
150
171
  **Parameters:** None
151
172
 
152
173
  **Returns:**
174
+
153
175
  - `dialogState: DialogState<TMessage> | null` - Current dialog state
154
176
  - `message: TMessage` - The message passed to confirm
155
177
  - `isOpen: boolean` - Whether dialog is open
156
- - `resolve: (value: boolean) => void` - Internal resolver
178
+ - `resolve: (value: boolean) => void` - Settle and close this specific dialog
157
179
  - `confirm: (message: TMessage) => Promise<boolean>` - Show dialog, returns Promise
158
180
  - `onConfirm: () => void` - Resolve dialog with `true`
159
181
  - `onCancel: () => void` - Resolve dialog with `false`
160
182
 
183
+ Each request settles once: confirmation resolves `true`; cancellation, replacement by
184
+ another `confirm` call, and hook unmount resolve `false`. Calling `dialogState.resolve`
185
+ also closes that dialog. Repeated calls and resolvers captured from an older dialog
186
+ have no effect on a newer dialog.
187
+
188
+ Effect teardown also cancels and clears the pending dialog when React preserves
189
+ hook state, such as when an `Activity` becomes hidden. Revealing the subtree starts
190
+ with no open dialog and allows new confirmation requests.
191
+
192
+ `confirm`, `onConfirm`, and `onCancel` retain stable references across renders.
193
+ The hook-level `onConfirm` and `onCancel` always act on the current dialog; use its
194
+ `dialogState.resolve` when a captured callback must belong to a specific dialog.
195
+
161
196
  **Example:**
162
197
 
163
198
  ```tsx
164
- import { useNavigationBlocker, useDialogState } from '@okyrychenko-dev/react-action-guard-router/react-router';
199
+ import {
200
+ useNavigationBlocker,
201
+ useDialogState,
202
+ } from "@okyrychenko-dev/react-action-guard-router/react-router";
165
203
 
166
204
  function MyComponent() {
167
205
  const [hasChanges, setHasChanges] = useState(false);
@@ -169,18 +207,14 @@ function MyComponent() {
169
207
 
170
208
  useNavigationBlocker({
171
209
  when: hasChanges,
172
- onConfirm: confirm, // Returns Promise<boolean>
210
+ onConfirm: confirm, // Returns Promise<boolean>
173
211
  });
174
212
 
175
213
  return (
176
214
  <>
177
215
  <form>...</form>
178
216
  {dialogState && (
179
- <CustomDialog
180
- message={dialogState.message}
181
- onConfirm={onConfirm}
182
- onCancel={onCancel}
183
- />
217
+ <CustomDialog message={dialogState.message} onConfirm={onConfirm} onCancel={onCancel} />
184
218
  )}
185
219
  </>
186
220
  );
@@ -193,37 +227,53 @@ function MyComponent() {
193
227
  interface CustomMessage {
194
228
  title: string;
195
229
  body: string;
196
- severity: 'warning' | 'error';
230
+ severity: "warning" | "error";
197
231
  }
198
232
 
199
233
  const { dialogState, confirm } = useDialogState<CustomMessage>();
200
234
 
201
235
  useNavigationBlocker({
202
236
  when: isDirty,
203
- onConfirm: () => confirm({
204
- title: 'Unsaved Changes',
205
- body: 'Your work will be lost',
206
- severity: 'warning',
207
- }),
237
+ onConfirm: () =>
238
+ confirm({
239
+ title: "Unsaved Changes",
240
+ body: "Your work will be lost",
241
+ severity: "warning",
242
+ }),
208
243
  });
209
244
  ```
210
245
 
246
+ ### Render-time conditions
247
+
248
+ A function passed as `when` must be a pure condition. Each React render evaluates
249
+ it again, including a stable function created with `useCallback` or one reading a
250
+ ref or external state. Changing a ref or external value alone does not trigger a
251
+ render or refresh protection; arrange a React rerender or subscription.
252
+ `useCallback` is optional and does not cache the condition's result. A true
253
+ condition or a blocked `scope` enables protection (OR semantics).
254
+
255
+ This correction affects consumers whose stable predicates previously retained a
256
+ stale result: protection now follows the current result after rerender. Router
257
+ capabilities are unchanged; Next App Router protects document unload only and
258
+ does not intercept same-document navigation.
259
+
211
260
  #### `usePrompt(message, when)` (React Router only)
212
261
 
213
262
  Simple API similar to React Router v5's `usePrompt`.
214
263
 
215
264
  **Parameters:**
265
+
216
266
  - `message: string` - Confirmation message
217
267
  - `when: boolean` - Condition to activate blocking
218
268
 
219
269
  **Example:**
220
270
 
221
271
  ```tsx
222
- import { usePrompt } from '@okyrychenko-dev/react-action-guard-router/react-router';
272
+ import { usePrompt } from "@okyrychenko-dev/react-action-guard-router/react-router";
223
273
 
224
274
  function MyForm() {
225
275
  const [hasChanges, setHasChanges] = useState(false);
226
- usePrompt('You have unsaved changes', hasChanges);
276
+ usePrompt("You have unsaved changes", hasChanges);
227
277
  }
228
278
  ```
229
279
 
@@ -232,17 +282,18 @@ function MyForm() {
232
282
  Standalone hook for blocking browser unload events (tab close, refresh).
233
283
 
234
284
  **Parameters:**
285
+
235
286
  - `when: boolean` - Condition to activate blocking
236
287
  - `message?: string` - Optional custom message (default: "Are you sure you want to leave?")
237
288
 
238
289
  **Example:**
239
290
 
240
291
  ```tsx
241
- import { useBeforeUnload } from '@okyrychenko-dev/react-action-guard-router';
292
+ import { useBeforeUnload } from "@okyrychenko-dev/react-action-guard-router";
242
293
 
243
294
  function MyComponent() {
244
295
  const [hasUnsavedWork, setHasUnsavedWork] = useState(false);
245
- useBeforeUnload(hasUnsavedWork, 'You have unsaved work');
296
+ useBeforeUnload(hasUnsavedWork, "You have unsaved work");
246
297
  }
247
298
  ```
248
299
 
@@ -257,12 +308,12 @@ This utility works independently of any router and can be used in any React appl
257
308
  Full support with React Router v6's `useBlocker` hook.
258
309
 
259
310
  ```tsx
260
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';
311
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/react-router";
261
312
 
262
313
  function MyComponent() {
263
314
  useNavigationBlocker({
264
315
  when: isDirty,
265
- message: 'Discard changes?',
316
+ message: "Discard changes?",
266
317
  });
267
318
  }
268
319
  ```
@@ -272,33 +323,39 @@ function MyComponent() {
272
323
 
273
324
  ### TanStack Router
274
325
 
275
- Full support with TanStack Router's history blocking.
326
+ Uses TanStack Router's native `useBlocker` for in-app transitions. Requires TanStack Router 1.170.41 or newer; earlier versions are no longer declared supported.
276
327
 
277
328
  ```tsx
278
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/tanstack-router';
329
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/tanstack-router";
279
330
 
280
331
  function MyComponent() {
281
332
  useNavigationBlocker({
282
333
  when: isDirty,
283
- message: 'Discard changes?',
334
+ message: "Discard changes?",
284
335
  });
285
336
  }
286
337
  ```
287
338
 
288
- Async `onConfirm` is supported and evaluated once per blocked navigation attempt.
339
+ Sync and async `onConfirm` are evaluated once per blocked navigation attempt. Active blocking without a message denies silently and emits `onBlock`. Denial, rejection, and thrown confirmation errors retain the current location. Acceptance permits that transition once and emits `onAllow`; subsequent transitions remain protected. Superseded confirmations and results received after unmount or authorization option changes cannot authorize navigation. Replacing `when` or changing the effective `scope` contents invalidates a pending confirmation even when the computed blocking condition remains true. Equivalent scope arrays preserve the attempt: array identity, order, and duplicates do not affect scope comparison. Changes to observer callbacks `onBlock` and `onAllow` do not invalidate a pending confirmation; the attempt retains the callbacks it started with.
340
+
341
+ `scope` and `when` retain their existing OR behavior. `blockBrowserUnload` controls native TanStack unload protection. Its one-shot bypass suppresses a second browser prompt after accepted document navigation; later unloads remain protected. The adapter does not register a separate shared unload handler. Unload uses a browser-controlled prompt, not asynchronous custom confirmation.
342
+
343
+ **Not-found limitation:** In the verified TanStack Router 1.170.41, navigation from an unmatched URL (`__notFound__`) to a matched route bypasses native `useBlocker` before this adapter's callback runs, even with `when: true` or an active scope. `onBlock`, `onConfirm`, and `onAllow` are not called for that transition. A blocker mounted above the not-found UI therefore cannot protect this exit; do not rely on it to guard unsaved work there. Subsequent matched-to-matched navigation remains guarded. This is upstream behavior introduced by [the upstream TanStack Router change](https://github.com/TanStack/router/pull/4917), covered by real-router tests for both condition sources. The adapter does not compensate for the bypass.
344
+
345
+ Compatibility tests exercise the public hook at 1.170.41 with real memory history for in-app navigation and browser history in the DOM test environment for unload protection. The unload tests intercept document location assignment and dispatch beforeunload events; they do not establish browser prompt UI behavior. Separate Playwright tests verified same-document back/forward on matched routes in Chrome 154.0.8037.57 on 2026-10-03: custom-dialog cancellation retains the original route and URL, confirmation reaches the intended destination once, and subsequent history attempts remain protected. This evidence does not establish other browsers or cross-document history transitions. Reproduction steps, evaluated versions, and result artifacts are documented in [browser-history verification](https://github.com/okyrychenko-dev/react-action-guard/blob/main/packages/router/browser/README.md). The verified floor was raised from 1.170.28 because the older locked router-core/history combination skipped blockers for external navigation. The native API is documented in [TanStack navigation blocking](https://tanstack.com/router/latest/docs/guide/navigation-blocking).
289
346
 
290
347
  ### Next.js Pages Router
291
348
 
292
- Full support with Next.js router events.
349
+ Limited interception through Next.js router events.
293
350
 
294
351
  ```tsx
295
352
  // pages/edit.tsx
296
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/nextjs';
353
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/nextjs";
297
354
 
298
355
  function EditPage() {
299
356
  useNavigationBlocker({
300
357
  when: isDirty,
301
- message: 'Discard changes?',
358
+ message: "Discard changes?",
302
359
  });
303
360
  }
304
361
  ```
@@ -306,39 +363,50 @@ function EditPage() {
306
363
  **Async `onConfirm` behavior:**
307
364
  Pages Router does not allow pausing transitions. When you return a Promise from `onConfirm`, the hook cancels the current navigation and re-attempts it via `router.push(url)` if confirmed. This may not preserve original transition options (e.g., `shallow`, `scroll`, `locale`).
308
365
 
366
+ Only the current confirmation attempt can resume navigation. Cleanup, disabling protection, or changing the protected scope or message invalidates pending answers. Callback rerenders and equivalent scopes preserve the current attempt. `onAllow` fires once for an accepted attempt; the replay permission is consumed once and cleared when the retry finishes or another URL is attempted.
367
+
368
+ Selected packed Next 15.5.23 production Pages/App browser evidence and its limitations are recorded in the [runtime guide](https://github.com/okyrychenko-dev/react-action-guard/blob/main/scripts/next-runtime/README.md) and [capability matrix](https://github.com/okyrychenko-dev/react-action-guard/blob/main/CAPABILITIES.md). This does not establish every supported peer or navigation option.
369
+
309
370
  ### Next.js App Router
310
371
 
311
- Best-effort support only. Browser unload protection works, but App Router does not expose an official navigation-blocking API.
372
+ Unload-only protection: the hook requests a browser `beforeunload` prompt, subject to browser policy. It does not intercept same-document `Link`, `push`, or back/forward navigation, and does not evaluate `onConfirm`. App Router does not expose an official navigation-blocking API.
312
373
 
313
374
  ```tsx
314
375
  // app/edit/page.tsx
315
- 'use client';
376
+ "use client";
316
377
 
317
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/nextjs';
378
+ import { useState } from "react";
379
+ import { useAppRouterBlocker } from "@okyrychenko-dev/react-action-guard-router/nextjs";
318
380
 
319
- function EditPage() {
320
- useNavigationBlocker({
381
+ export default function EditPage() {
382
+ const [isDirty, setIsDirty] = useState(false);
383
+
384
+ useAppRouterBlocker({
321
385
  when: isDirty,
322
- message: 'You have unsaved changes',
386
+ message: "You have unsaved changes",
323
387
  });
388
+
389
+ return <input onChange={() => setIsDirty(true)} />;
324
390
  }
325
391
  ```
326
392
 
327
393
  **App Router Limitations:**
328
- - ✅ Browser back/forward/close/refresh are blocked
394
+
395
+ - `beforeunload` can request the browser’s native prompt for document unload (close, refresh, or cross-document navigation), subject to browser policy.
396
+ - Same-document App Router back/forward is not intercepted.
329
397
  - ❌ `<Link>` component navigation is NOT blocked
330
398
  - ❌ `router.push()` is NOT blocked
331
399
 
332
- For full navigation blocking support, use Pages Router.
400
+ Pages Router provides route-event interception with the replay limitations described above.
333
401
 
334
402
  ## Adapter Capabilities
335
403
 
336
- | Adapter | `isBlocking` meaning | `isIntercepting` | Async `onConfirm` | Caveats |
337
- | --- | --- | --- | --- | --- |
338
- | React Router | Blocking condition is armed | Yes | Yes | Best semantic fidelity |
339
- | TanStack Router | Blocking condition is armed | No | Yes | Depends on history.block integration |
340
- | Next.js Pages Router | Blocking condition is armed | No | Yes | Re-attempts confirmed navigation with `router.push(url)` |
341
- | Next.js App Router | Blocking condition is armed | No | Best effort only | No official blocker API from Next.js |
404
+ | Adapter | `isBlocking` meaning | `isIntercepting` | Async `onConfirm` | Caveats |
405
+ | -------------------- | --------------------------- | ---------------- | ----------------- | -------------------------------------------------------- |
406
+ | React Router | Blocking condition is armed | Yes | Yes | Best semantic fidelity |
407
+ | TanStack Router | Blocking condition is armed | No | Yes | Native useBlocker; memory-history transitions verified |
408
+ | Next.js Pages Router | Blocking condition is armed | No | Yes | Re-attempts confirmed navigation with `router.push(url)` |
409
+ | Next.js App Router | Blocking condition is armed | No | No | beforeunload only; no same-document interception |
342
410
 
343
411
  ---
344
412
 
@@ -353,7 +421,7 @@ function SignupForm() {
353
421
 
354
422
  useNavigationBlocker({
355
423
  when: isDirty,
356
- message: 'Your signup progress will be lost. Continue?',
424
+ message: "Your signup progress will be lost. Continue?",
357
425
  });
358
426
  }
359
427
  ```
@@ -366,14 +434,18 @@ function MultiStepWizard() {
366
434
  const [isSubmitting, setIsSubmitting] = useState(false);
367
435
 
368
436
  // Block UI during submission
369
- useBlocker('wizard-submit', {
370
- scope: 'wizard',
371
- reason: 'Submitting form...',
372
- }, isSubmitting);
437
+ useActionBlocker(
438
+ "wizard-submit",
439
+ {
440
+ scope: "wizard",
441
+ reason: "Submitting form...",
442
+ },
443
+ isSubmitting
444
+ );
373
445
 
374
446
  // Block navigation during submission OR if wizard incomplete
375
447
  useNavigationBlocker({
376
- scope: 'wizard',
448
+ scope: "wizard",
377
449
  when: currentStep < 5,
378
450
  message: `You're on step ${currentStep}/5. Exit wizard?`,
379
451
  });
@@ -383,22 +455,24 @@ function MultiStepWizard() {
383
455
  ### E-Commerce Checkout
384
456
 
385
457
  ```tsx
386
- import { useBlockingMutation } from '@okyrychenko-dev/react-action-guard-tanstack';
387
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';
458
+ import { useBlockingMutation } from "@okyrychenko-dev/react-action-guard-tanstack";
459
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/react-router";
388
460
 
389
461
  function CheckoutPage() {
390
462
  const paymentMutation = useBlockingMutation({
391
463
  mutationFn: processPayment,
392
- blockingConfig: { scope: 'checkout' },
464
+ blockingConfig: { scope: "checkout" },
393
465
  });
394
466
 
395
467
  useNavigationBlocker({
396
- scope: 'checkout',
397
- message: 'Payment is processing. Leaving will cancel the transaction.',
468
+ scope: "checkout",
469
+ message: "Payment is processing. Leave this page?",
398
470
  });
399
471
  }
400
472
  ```
401
473
 
474
+ UI blocking and navigation confirmation do not cancel the payment request or roll back a transaction. Cancellation, server authorization, and idempotency belong to the application and its backend.
475
+
402
476
  ### File Upload with Progress
403
477
 
404
478
  ```tsx
@@ -423,31 +497,29 @@ TypeScript-friendly API surface:
423
497
  ```typescript
424
498
  import type {
425
499
  // Core types
426
- UseNavigationBlockerOptions,
500
+ BaseNavigationBlockerOptions,
427
501
  NavigationBlockerReturn,
428
502
  DialogState,
429
- ConfirmationResult,
430
- ConfirmationCallbacks,
431
- } from '@okyrychenko-dev/react-action-guard-router';
503
+ } from "@okyrychenko-dev/react-action-guard-router";
432
504
 
433
505
  // Router-specific types
434
- import type { UseNavigationBlockerOptions } from '@okyrychenko-dev/react-action-guard-router/react-router';
506
+ import type { UseNavigationBlockerOptions } from "@okyrychenko-dev/react-action-guard-router/react-router";
435
507
  ```
436
508
 
437
509
  **Typed scopes with `react-action-guard`:**
438
510
 
439
511
  ```tsx
440
- import { createTypedHooks } from '@okyrychenko-dev/react-action-guard';
512
+ import { createTypedHooks } from "@okyrychenko-dev/react-action-guard";
441
513
 
442
- type AppScopes = 'form' | 'checkout' | 'navigation';
443
- const { useBlocker } = createTypedHooks<AppScopes>();
514
+ type AppScopes = "form" | "checkout" | "navigation";
515
+ const { useActionBlocker } = createTypedHooks<AppScopes>();
444
516
 
445
517
  function MyComponent() {
446
- useBlocker('id', { scope: 'form' }); // ✅ Typed in the core package
518
+ useActionBlocker("id", { scope: "form" }); // ✅ Typed in the core package
447
519
 
448
520
  useNavigationBlocker({
449
- scope: 'form', // ✅ Reuses the same scope values cleanly
450
- message: 'Leave form?',
521
+ scope: "form", // ✅ Reuses the same scope values cleanly
522
+ message: "Leave form?",
451
523
  });
452
524
  }
453
525
  ```
@@ -460,10 +532,11 @@ Tree-shakeable by router adapter. Import only what you need:
460
532
 
461
533
  ```tsx
462
534
  // Only React Router code is bundled
463
- import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router/react-router';
535
+ import { useNavigationBlocker } from "@okyrychenko-dev/react-action-guard-router/react-router";
464
536
  ```
465
537
 
466
538
  **Approximate sizes (minified):**
539
+
467
540
  - Core utilities: ~2 KB
468
541
  - React Router adapter: ~2.5 KB
469
542
  - TanStack Router adapter: ~2.5 KB
@@ -471,26 +544,27 @@ import { useNavigationBlocker } from '@okyrychenko-dev/react-action-guard-router
471
544
 
472
545
  ---
473
546
 
474
- ## Development
475
-
476
- ```bash
477
- # Install dependencies
478
- npm install
547
+ </details>
479
548
 
480
- # Run tests
481
- npm run test
482
-
483
- # Build the package
484
- npm run build
485
-
486
- # Type checking
487
- npm run typecheck
549
+ ## Development
488
550
 
489
- # Lint
490
- npm run lint
551
+ This package lives in the [react-action-guard monorepo](https://github.com/okyrychenko-dev/react-action-guard)
552
+ (pnpm workspaces). From the monorepo root:
491
553
 
492
- # Fix lint errors
493
- npm run lint:fix
554
+ ```bash
555
+ # Install dependencies for all packages
556
+ pnpm install
557
+
558
+ # Run this package's scripts with --filter
559
+ pnpm --filter @okyrychenko-dev/react-action-guard-router run test:run
560
+ pnpm --filter @okyrychenko-dev/react-action-guard-router run build
561
+ pnpm --filter @okyrychenko-dev/react-action-guard-router run typecheck
562
+ pnpm --filter @okyrychenko-dev/react-action-guard-router run lint
563
+ pnpm --filter @okyrychenko-dev/react-action-guard-router run lint:fix
564
+
565
+ # Or cd into the package and run scripts directly
566
+ cd packages/router
567
+ pnpm run test:run
494
568
  ```
495
569
 
496
570
  ---
@@ -505,12 +579,15 @@ npm run lint:fix
505
579
 
506
580
  ## Contributing
507
581
 
508
- Contributions are welcome! Please ensure:
582
+ Contributions are welcome! Please ensure, before opening a PR:
509
583
 
510
- 1. Tests pass (`npm run test:run`)
511
- 2. Code is properly typed (`npm run typecheck`)
512
- 3. Linting passes (`npm run lint`)
513
- 4. Code is formatted (`npm run format`)
584
+ 1. Tests pass (`pnpm run test:run`)
585
+ 2. Code is properly typed (`pnpm run typecheck`)
586
+ 3. Linting passes (`pnpm run lint`)
587
+ 4. Code is formatted (`pnpm run format`)
588
+ 5. If the change affects this package's public behavior, add a changeset: `pnpm changeset`
589
+ (see the [Releasing](https://github.com/okyrychenko-dev/react-action-guard#releasing) section
590
+ of the monorepo README)
514
591
 
515
592
  ---
516
593