@okyrychenko-dev/react-action-guard 0.2.1 → 0.2.3

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,5 +1,9 @@
1
1
  # @okyrychenko-dev/react-action-guard
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@okyrychenko-dev/react-action-guard.svg)](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@okyrychenko-dev/react-action-guard.svg)](https://www.npmjs.com/package/@okyrychenko-dev/react-action-guard)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  > Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup
4
8
 
5
9
  ## Features
@@ -7,8 +11,15 @@
7
11
  - Priority-based blocking system
8
12
  - Scoped blocking (global, specific areas, or multiple scopes)
9
13
  - Automatic cleanup on unmount
10
- - TypeScript support
14
+ - Advanced hooks for different use cases:
15
+ - Confirmable blockers with custom dialogs
16
+ - Scheduled blocking for maintenance windows
17
+ - Conditional blocking based on application state
18
+ - Async action wrapping
19
+ - Advanced middleware system for analytics, logging, and performance monitoring
20
+ - TypeScript support with full type safety
11
21
  - Built on Zustand for efficient state management
22
+ - Tree-shakeable - import only what you need
12
23
  - Hooks-based API
13
24
 
14
25
  ## Installation
@@ -29,6 +40,16 @@ function MyComponent() {
29
40
  }
30
41
  ```
31
42
 
43
+ ## Documentation
44
+
45
+ 📚 **[Interactive Storybook Documentation](https://your-storybook-url.com)** - Explore live examples and detailed guides for all hooks
46
+
47
+ To run Storybook locally:
48
+
49
+ ```bash
50
+ npm run storybook
51
+ ```
52
+
32
53
  ## API Reference
33
54
 
34
55
  ### Hooks
@@ -107,6 +128,138 @@ function MyComponent() {
107
128
  }
108
129
  ```
109
130
 
131
+ #### `useConfirmableBlocker(blockerId, config, options)`
132
+
133
+ Creates a blocker that requires user confirmation before being removed.
134
+
135
+ **Parameters:**
136
+
137
+ - `blockerId: string` - Unique identifier for the blocker
138
+ - `config: BlockerConfig` - Configuration object
139
+ - `options: ConfirmableBlockerOptions`
140
+ - `enabled?: boolean` - Whether the blocker is active (default: true)
141
+ - `confirmMessage?: string` - Message to show in confirmation dialog
142
+ - `onConfirm?: () => void | Promise<void>` - Callback when user confirms
143
+
144
+ **Returns:**
145
+
146
+ - `isBlocking: boolean` - Whether the blocker is currently active
147
+ - `cancel: () => void` - Manually remove the blocker without confirmation
148
+ - `requestRemoval: () => Promise<boolean>` - Request removal with confirmation
149
+
150
+ **Example:**
151
+
152
+ ```jsx
153
+ function UnsavedChangesGuard() {
154
+ const { isBlocking, requestRemoval } = useConfirmableBlocker(
155
+ "unsaved-changes",
156
+ {
157
+ scope: "navigation",
158
+ reason: "Unsaved changes",
159
+ },
160
+ {
161
+ enabled: hasUnsavedChanges,
162
+ confirmMessage: "You have unsaved changes. Are you sure you want to leave?",
163
+ onConfirm: async () => {
164
+ await discardChanges();
165
+ },
166
+ }
167
+ );
168
+
169
+ return (
170
+ <div>
171
+ {isBlocking && <UnsavedIndicator />}
172
+ <button onClick={requestRemoval}>Leave</button>
173
+ </div>
174
+ );
175
+ }
176
+ ```
177
+
178
+ #### `useScheduledBlocker(blockerId, config)`
179
+
180
+ Blocks UI during a scheduled time period or maintenance window.
181
+
182
+ **Parameters:**
183
+
184
+ - `blockerId: string` - Unique identifier for the blocker
185
+ - `config: ScheduledBlockerConfig`
186
+ - `schedule: BlockingSchedule`
187
+ - `start: string | Date | number` - Start time (ISO string, Date, or timestamp)
188
+ - `end?: string | Date | number` - End time (optional)
189
+ - `duration?: number` - Duration in milliseconds (takes precedence over end)
190
+ - `onScheduleStart?: () => void` - Callback when blocking starts
191
+ - `onScheduleEnd?: () => void` - Callback when blocking ends
192
+ - Plus all `BlockerConfig` properties (scope, reason, priority)
193
+
194
+ **Example:**
195
+
196
+ ```jsx
197
+ function MaintenanceWindow() {
198
+ useScheduledBlocker("maintenance", {
199
+ scope: "global",
200
+ reason: "Scheduled maintenance",
201
+ priority: 1000,
202
+ schedule: {
203
+ start: "2024-01-15T02:00:00Z",
204
+ duration: 3600000, // 1 hour in milliseconds
205
+ },
206
+ onScheduleStart: () => {
207
+ console.log("Maintenance started");
208
+ },
209
+ onScheduleEnd: () => {
210
+ console.log("Maintenance completed");
211
+ },
212
+ });
213
+
214
+ return <div>App content</div>;
215
+ }
216
+ ```
217
+
218
+ #### `useConditionalBlocker(blockerId, config)`
219
+
220
+ Periodically checks a condition and blocks/unblocks based on the result.
221
+
222
+ **Parameters:**
223
+
224
+ - `blockerId: string` - Unique identifier for the blocker
225
+ - `config: ConditionalBlockerConfig<TState>`
226
+ - `scope: string | string[]` - Required scope(s) to block
227
+ - `condition: (state?: TState) => boolean` - Function that determines if blocking should be active
228
+ - `checkInterval?: number` - How often to check the condition in ms (default: 1000)
229
+ - `state?: TState` - Optional state to pass to the condition function
230
+ - Plus all other `BlockerConfig` properties (reason, priority)
231
+
232
+ **Example:**
233
+
234
+ ```jsx
235
+ function NetworkStatusBlocker() {
236
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
237
+
238
+ useEffect(() => {
239
+ const handleOnline = () => setIsOnline(true);
240
+ const handleOffline = () => setIsOnline(false);
241
+
242
+ window.addEventListener("online", handleOnline);
243
+ window.addEventListener("offline", handleOffline);
244
+
245
+ return () => {
246
+ window.removeEventListener("online", handleOnline);
247
+ window.removeEventListener("offline", handleOffline);
248
+ };
249
+ }, []);
250
+
251
+ useConditionalBlocker("network-check", {
252
+ scope: ["form", "navigation"],
253
+ reason: "No network connection",
254
+ priority: 100,
255
+ condition: () => !isOnline,
256
+ checkInterval: 2000,
257
+ });
258
+
259
+ return <div>App content</div>;
260
+ }
261
+ ```
262
+
110
263
  ### Store
111
264
 
112
265
  #### `useUIBlockingStore`
@@ -117,6 +270,7 @@ Direct access to the Zustand store for advanced use cases.
117
270
 
118
271
  - `addBlocker(id, config)` - Manually add a blocker
119
272
  - `removeBlocker(id)` - Manually remove a blocker
273
+ - `updateBlocker(id, config)` - Update an existing blocker
120
274
  - `isBlocked(scope)` - Check if scope is blocked
121
275
  - `getBlockingInfo(scope)` - Get detailed blocking information
122
276
  - `clearAllBlockers()` - Remove all blockers
@@ -128,7 +282,7 @@ Direct access to the Zustand store for advanced use cases.
128
282
  import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
129
283
 
130
284
  function AdvancedComponent() {
131
- const { addBlocker, removeBlocker } = useUIBlockingStore();
285
+ const { addBlocker, removeBlocker, updateBlocker } = useUIBlockingStore();
132
286
 
133
287
  const startBlocking = () => {
134
288
  addBlocker("custom-blocker", {
@@ -138,6 +292,12 @@ function AdvancedComponent() {
138
292
  });
139
293
  };
140
294
 
295
+ const updatePriority = () => {
296
+ updateBlocker("custom-blocker", {
297
+ priority: 200,
298
+ });
299
+ };
300
+
141
301
  const stopBlocking = () => {
142
302
  removeBlocker("custom-blocker");
143
303
  };
@@ -145,18 +305,159 @@ function AdvancedComponent() {
145
305
  return (
146
306
  <div>
147
307
  <button onClick={startBlocking}>Start</button>
308
+ <button onClick={updatePriority}>Increase Priority</button>
148
309
  <button onClick={stopBlocking}>Stop</button>
149
310
  </div>
150
311
  );
151
312
  }
152
313
  ```
153
314
 
315
+ ## Middleware System
316
+
317
+ The library includes a powerful middleware system that allows you to hook into blocker lifecycle events for analytics, logging, and performance monitoring.
318
+
319
+ ### Built-in Middleware
320
+
321
+ #### Analytics Middleware
322
+
323
+ Track blocker events with your analytics provider (Google Analytics, Mixpanel, Amplitude, or custom).
324
+
325
+ ```jsx
326
+ import { configureMiddleware, analyticsMiddleware } from "@okyrychenko-dev/react-action-guard";
327
+
328
+ // Google Analytics
329
+ configureMiddleware([analyticsMiddleware({ provider: "ga" })]);
330
+
331
+ // Mixpanel
332
+ configureMiddleware([analyticsMiddleware({ provider: "mixpanel" })]);
333
+
334
+ // Amplitude
335
+ configureMiddleware([analyticsMiddleware({ provider: "amplitude" })]);
336
+
337
+ // Custom analytics
338
+ configureMiddleware([
339
+ analyticsMiddleware({
340
+ track: (event, data) => {
341
+ myAnalytics.track(event, data);
342
+ },
343
+ }),
344
+ ]);
345
+ ```
346
+
347
+ #### Logger Middleware
348
+
349
+ Log blocker lifecycle events to the console for debugging.
350
+
351
+ ```jsx
352
+ import { configureMiddleware, loggerMiddleware } from "@okyrychenko-dev/react-action-guard";
353
+
354
+ configureMiddleware([loggerMiddleware]);
355
+ ```
356
+
357
+ #### Performance Middleware
358
+
359
+ Monitor blocker performance and detect slow operations.
360
+
361
+ ```jsx
362
+ import { configureMiddleware, performanceMiddleware } from "@okyrychenko-dev/react-action-guard";
363
+
364
+ configureMiddleware([
365
+ performanceMiddleware({
366
+ onSlowBlock: (blockerId, duration) => {
367
+ console.warn(`Blocker ${blockerId} was active for ${duration}ms`);
368
+ },
369
+ slowBlockThreshold: 5000, // 5 seconds
370
+ }),
371
+ ]);
372
+ ```
373
+
374
+ ### Custom Middleware
375
+
376
+ Create your own middleware to handle blocker events:
377
+
378
+ ```jsx
379
+ import { configureMiddleware } from "@okyrychenko-dev/react-action-guard";
380
+
381
+ const myCustomMiddleware = (context) => {
382
+ const { action, blockerId, config, timestamp } = context;
383
+
384
+ if (action === "add") {
385
+ console.log(`Blocker added: ${blockerId}`, config);
386
+ } else if (action === "remove") {
387
+ console.log(`Blocker removed: ${blockerId}`);
388
+ } else if (action === "update") {
389
+ console.log(`Blocker updated: ${blockerId}`, config);
390
+ }
391
+ };
392
+
393
+ configureMiddleware([myCustomMiddleware]);
394
+ ```
395
+
396
+ ### Combining Middleware
397
+
398
+ You can combine multiple middleware for comprehensive monitoring:
399
+
400
+ ```jsx
401
+ import {
402
+ configureMiddleware,
403
+ analyticsMiddleware,
404
+ loggerMiddleware,
405
+ performanceMiddleware,
406
+ } from "@okyrychenko-dev/react-action-guard";
407
+
408
+ configureMiddleware([
409
+ loggerMiddleware,
410
+ analyticsMiddleware({ provider: "ga" }),
411
+ performanceMiddleware({
412
+ slowBlockThreshold: 3000,
413
+ onSlowBlock: (blockerId, duration) => {
414
+ // Send to error tracking service
415
+ errorTracker.captureMessage(`Slow blocker: ${blockerId}`, {
416
+ duration,
417
+ });
418
+ },
419
+ }),
420
+ ]);
421
+ ```
422
+
423
+ ## Tree Shaking
424
+
425
+ The library is fully tree-shakeable. Import only the features you need to keep your bundle size small:
426
+
427
+ ```jsx
428
+ // Only imports the hook you need
429
+ import { useBlocker } from "@okyrychenko-dev/react-action-guard";
430
+
431
+ // Middleware is not included unless you import it
432
+ import { configureMiddleware, analyticsMiddleware } from "@okyrychenko-dev/react-action-guard";
433
+ ```
434
+
435
+ The package is configured with `"sideEffects": false`, allowing modern bundlers (Webpack, Rollup, Vite) to eliminate unused code automatically.
436
+
154
437
  ## TypeScript
155
438
 
156
439
  The package is written in TypeScript and includes full type definitions.
157
440
 
158
441
  ```typescript
159
- import type { BlockerConfig, BlockerInfo, UIBlockingState } from "@okyrychenko-dev/react-action-guard";
442
+ import type {
443
+ // Core types
444
+ BlockerConfig,
445
+ BlockerInfo,
446
+ UIBlockingState,
447
+
448
+ // Hook types
449
+ ConfirmableBlockerOptions,
450
+ ScheduledBlockerConfig,
451
+ ConditionalBlockerConfig,
452
+ BlockingSchedule,
453
+
454
+ // Middleware types
455
+ Middleware,
456
+ MiddlewareContext,
457
+ AnalyticsConfig,
458
+ AnalyticsProvider,
459
+ PerformanceMiddlewareConfig,
460
+ } from "@okyrychenko-dev/react-action-guard";
160
461
  ```
161
462
 
162
463
  ## Use Cases
@@ -180,9 +481,11 @@ function DataLoader() {
180
481
  }
181
482
  ```
182
483
 
183
- ### Form Submission
484
+ ### Form Submission with Analytics
184
485
 
185
486
  ```jsx
487
+ import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
488
+
186
489
  function UserForm() {
187
490
  const executeWithBlocking = useAsyncAction("submit-form", "form");
188
491
  const isBlocked = useIsBlocked("form");
@@ -202,6 +505,46 @@ function UserForm() {
202
505
  }
203
506
  ```
204
507
 
508
+ ### Unsaved Changes Protection
509
+
510
+ ```jsx
511
+ import { useConfirmableBlocker } from "@okyrychenko-dev/react-action-guard";
512
+
513
+ function FormWithUnsavedWarning() {
514
+ const [formData, setFormData] = useState({});
515
+ const [hasChanges, setHasChanges] = useState(false);
516
+
517
+ const { requestRemoval } = useConfirmableBlocker(
518
+ "unsaved-form",
519
+ {
520
+ scope: "navigation",
521
+ reason: "Unsaved form data",
522
+ priority: 100,
523
+ },
524
+ {
525
+ enabled: hasChanges,
526
+ confirmMessage: "You have unsaved changes. Discard them?",
527
+ onConfirm: () => {
528
+ setFormData({});
529
+ setHasChanges(false);
530
+ },
531
+ }
532
+ );
533
+
534
+ return (
535
+ <form>
536
+ <input
537
+ onChange={(e) => {
538
+ setFormData({ ...formData, name: e.target.value });
539
+ setHasChanges(true);
540
+ }}
541
+ />
542
+ <button onClick={requestRemoval}>Cancel</button>
543
+ </form>
544
+ );
545
+ }
546
+ ```
547
+
205
548
  ### Global Loading Overlay
206
549
 
207
550
  ```jsx
@@ -217,22 +560,138 @@ function App() {
217
560
  }
218
561
  ```
219
562
 
563
+ ### Multi-Step Process with Priority
564
+
565
+ ```jsx
566
+ function MultiStepWizard() {
567
+ const [step, setStep] = useState(1);
568
+
569
+ // Higher priority for payment step
570
+ useBlocker(
571
+ "payment-step",
572
+ {
573
+ scope: ["navigation", "form"],
574
+ reason: "Processing payment",
575
+ priority: 100,
576
+ },
577
+ step === 3
578
+ );
579
+
580
+ // Lower priority for other steps
581
+ useBlocker(
582
+ "wizard-step",
583
+ {
584
+ scope: "navigation",
585
+ reason: "Wizard in progress",
586
+ priority: 50,
587
+ },
588
+ step < 3
589
+ );
590
+
591
+ return <div>Step {step}</div>;
592
+ }
593
+ ```
594
+
595
+ ### Scheduled Maintenance Window
596
+
597
+ ```jsx
598
+ import { useScheduledBlocker } from "@okyrychenko-dev/react-action-guard";
599
+
600
+ function App() {
601
+ useScheduledBlocker("weekly-maintenance", {
602
+ scope: "global",
603
+ reason: "Weekly system maintenance",
604
+ priority: 500,
605
+ schedule: {
606
+ start: new Date("2024-01-21T03:00:00Z"),
607
+ duration: 1800000, // 30 minutes
608
+ },
609
+ onScheduleStart: () => {
610
+ showNotification("System maintenance in progress");
611
+ },
612
+ onScheduleEnd: () => {
613
+ showNotification("Maintenance completed");
614
+ window.location.reload();
615
+ },
616
+ });
617
+
618
+ return <YourApp />;
619
+ }
620
+ ```
621
+
622
+ ### Dynamic Blocking Based on State
623
+
624
+ ```jsx
625
+ import { useConditionalBlocker } from "@okyrychenko-dev/react-action-guard";
626
+
627
+ function StorageQuotaGuard() {
628
+ const [storageUsed, setStorageUsed] = useState(0);
629
+ const STORAGE_LIMIT = 1000000; // 1MB
630
+
631
+ useConditionalBlocker("storage-limit", {
632
+ scope: ["upload", "save"],
633
+ reason: "Storage quota exceeded",
634
+ priority: 200,
635
+ condition: () => storageUsed > STORAGE_LIMIT,
636
+ state: storageUsed,
637
+ checkInterval: 5000, // Check every 5 seconds
638
+ });
639
+
640
+ return (
641
+ <div>
642
+ <p>
643
+ Storage used: {storageUsed} / {STORAGE_LIMIT} bytes
644
+ </p>
645
+ <UploadButton />
646
+ </div>
647
+ );
648
+ }
649
+ ```
650
+
220
651
  ## Development
221
652
 
222
653
  ```bash
223
654
  # Install dependencies
224
655
  npm install
225
656
 
657
+ # Run tests
658
+ npm run test
659
+
660
+ # Run tests with coverage
661
+ npm run test:coverage
662
+
226
663
  # Build the package
227
664
  npm run build
228
665
 
229
666
  # Type checking
230
667
  npm run typecheck
231
668
 
669
+ # Lint code
670
+ npm run lint
671
+
672
+ # Fix lint errors
673
+ npm run lint:fix
674
+
675
+ # Format code
676
+ npm run format
677
+
232
678
  # Watch mode for development
233
679
  npm run dev
234
680
  ```
235
681
 
682
+ ## Contributing
683
+
684
+ Contributions are welcome! Please ensure:
685
+
686
+ 1. All tests pass (`npm run test`)
687
+ 2. Code is properly typed (`npm run typecheck`)
688
+ 3. Linting passes (`npm run lint`)
689
+ 4. Code is formatted (`npm run format`)
690
+
691
+ ## Changelog
692
+
693
+ See [CHANGELOG.md](./CHANGELOG.md) for a detailed list of changes in each version.
694
+
236
695
  ## License
237
696
 
238
697
  MIT © Olexii Kyrychenko