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

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