@okyrychenko-dev/react-action-guard 0.2.0 → 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 +496 -37
  2. package/package.json +2 -2
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
@@ -20,16 +27,12 @@ npm install @okyrychenko-dev/react-action-guard zustand
20
27
  ## Quick Start
21
28
 
22
29
  ```jsx
23
- import { useBlocker, useIsBlocked } from '@okyrychenko-dev/react-action-guard';
30
+ import { useBlocker, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
24
31
 
25
32
  function MyComponent() {
26
- const isBlocked = useIsBlocked('my-scope');
33
+ const isBlocked = useIsBlocked("my-scope");
27
34
 
28
- return (
29
- <button disabled={isBlocked}>
30
- Click me
31
- </button>
32
- );
35
+ return <button disabled={isBlocked}>Click me</button>;
33
36
  }
34
37
  ```
35
38
 
@@ -42,6 +45,7 @@ function MyComponent() {
42
45
  Automatically adds a blocker when the component mounts and removes it on unmount.
43
46
 
44
47
  **Parameters:**
48
+
45
49
  - `blockerId: string` - Unique identifier for the blocker
46
50
  - `config: BlockerConfig` - Configuration object
47
51
  - `scope?: string | string[]` - Scope(s) to block (default: "global")
@@ -53,10 +57,10 @@ Automatically adds a blocker when the component mounts and removes it on unmount
53
57
 
54
58
  ```jsx
55
59
  function MyComponent() {
56
- useBlocker('my-blocker', {
57
- scope: 'form',
58
- reason: 'Form is saving',
59
- priority: 10
60
+ useBlocker("my-blocker", {
61
+ scope: "form",
62
+ reason: "Form is saving",
63
+ priority: 10,
60
64
  });
61
65
 
62
66
  return <div>Content</div>;
@@ -68,6 +72,7 @@ function MyComponent() {
68
72
  Checks if a specific scope (or scopes) is currently blocked.
69
73
 
70
74
  **Parameters:**
75
+
71
76
  - `scope?: string | string[]` - Scope(s) to check (default: "global")
72
77
 
73
78
  **Returns:** `boolean` - Whether the scope is blocked
@@ -76,13 +81,9 @@ Checks if a specific scope (or scopes) is currently blocked.
76
81
 
77
82
  ```jsx
78
83
  function SubmitButton() {
79
- const isFormBlocked = useIsBlocked('form');
84
+ const isFormBlocked = useIsBlocked("form");
80
85
 
81
- return (
82
- <button disabled={isFormBlocked}>
83
- Submit
84
- </button>
85
- );
86
+ return <button disabled={isFormBlocked}>Submit</button>;
86
87
  }
87
88
  ```
88
89
 
@@ -91,6 +92,7 @@ function SubmitButton() {
91
92
  Wraps an async function with automatic blocking/unblocking.
92
93
 
93
94
  **Parameters:**
95
+
94
96
  - `actionId: string` - Identifier for the action
95
97
  - `scope?: string | string[]` - Scope(s) to block during execution
96
98
 
@@ -100,7 +102,7 @@ Wraps an async function with automatic blocking/unblocking.
100
102
 
101
103
  ```jsx
102
104
  function MyComponent() {
103
- const executeWithBlocking = useAsyncAction('save-data', 'form');
105
+ const executeWithBlocking = useAsyncAction("save-data", "form");
104
106
 
105
107
  const handleSave = async () => {
106
108
  await executeWithBlocking(async () => {
@@ -112,6 +114,138 @@ function MyComponent() {
112
114
  }
113
115
  ```
114
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
+
115
249
  ### Store
116
250
 
117
251
  #### `useUIBlockingStore`
@@ -119,8 +253,10 @@ function MyComponent() {
119
253
  Direct access to the Zustand store for advanced use cases.
120
254
 
121
255
  **Methods:**
256
+
122
257
  - `addBlocker(id, config)` - Manually add a blocker
123
258
  - `removeBlocker(id)` - Manually remove a blocker
259
+ - `updateBlocker(id, config)` - Update an existing blocker
124
260
  - `isBlocked(scope)` - Check if scope is blocked
125
261
  - `getBlockingInfo(scope)` - Get detailed blocking information
126
262
  - `clearAllBlockers()` - Remove all blockers
@@ -129,42 +265,185 @@ Direct access to the Zustand store for advanced use cases.
129
265
  **Example:**
130
266
 
131
267
  ```jsx
132
- import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
268
+ import { useUIBlockingStore } from "@okyrychenko-dev/react-action-guard";
133
269
 
134
270
  function AdvancedComponent() {
135
- const { addBlocker, removeBlocker } = useUIBlockingStore();
271
+ const { addBlocker, removeBlocker, updateBlocker } = useUIBlockingStore();
136
272
 
137
273
  const startBlocking = () => {
138
- addBlocker('custom-blocker', {
139
- scope: ['form', 'navigation'],
140
- reason: 'Critical operation in progress',
141
- priority: 100
274
+ addBlocker("custom-blocker", {
275
+ scope: ["form", "navigation"],
276
+ reason: "Critical operation in progress",
277
+ priority: 100,
278
+ });
279
+ };
280
+
281
+ const updatePriority = () => {
282
+ updateBlocker("custom-blocker", {
283
+ priority: 200,
142
284
  });
143
285
  };
144
286
 
145
287
  const stopBlocking = () => {
146
- removeBlocker('custom-blocker');
288
+ removeBlocker("custom-blocker");
147
289
  };
148
290
 
149
291
  return (
150
292
  <div>
151
293
  <button onClick={startBlocking}>Start</button>
294
+ <button onClick={updatePriority}>Increase Priority</button>
152
295
  <button onClick={stopBlocking}>Stop</button>
153
296
  </div>
154
297
  );
155
298
  }
156
299
  ```
157
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
+
158
423
  ## TypeScript
159
424
 
160
425
  The package is written in TypeScript and includes full type definitions.
161
426
 
162
427
  ```typescript
163
428
  import type {
429
+ // Core types
164
430
  BlockerConfig,
165
431
  BlockerInfo,
166
- UIBlockingState
167
- } from '@okyrychenko-dev/react-action-guard';
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";
168
447
  ```
169
448
 
170
449
  ## Use Cases
@@ -175,21 +454,27 @@ import type {
175
454
  function DataLoader() {
176
455
  const [isLoading, setIsLoading] = useState(false);
177
456
 
178
- useBlocker('data-loader', {
179
- scope: 'content',
180
- reason: 'Loading data'
181
- }, isLoading);
457
+ useBlocker(
458
+ "data-loader",
459
+ {
460
+ scope: "content",
461
+ reason: "Loading data",
462
+ },
463
+ isLoading
464
+ );
182
465
 
183
466
  // ... rest of component
184
467
  }
185
468
  ```
186
469
 
187
- ### Form Submission
470
+ ### Form Submission with Analytics
188
471
 
189
472
  ```jsx
473
+ import { useAsyncAction, useIsBlocked } from "@okyrychenko-dev/react-action-guard";
474
+
190
475
  function UserForm() {
191
- const executeWithBlocking = useAsyncAction('submit-form', 'form');
192
- const isBlocked = useIsBlocked('form');
476
+ const executeWithBlocking = useAsyncAction("submit-form", "form");
477
+ const isBlocked = useIsBlocked("form");
193
478
 
194
479
  const handleSubmit = async (data) => {
195
480
  await executeWithBlocking(async () => {
@@ -206,11 +491,51 @@ function UserForm() {
206
491
  }
207
492
  ```
208
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
+
209
534
  ### Global Loading Overlay
210
535
 
211
536
  ```jsx
212
537
  function App() {
213
- const isGloballyBlocked = useIsBlocked('global');
538
+ const isGloballyBlocked = useIsBlocked("global");
214
539
 
215
540
  return (
216
541
  <div>
@@ -221,22 +546,156 @@ function App() {
221
546
  }
222
547
  ```
223
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
+
224
637
  ## Development
225
638
 
226
639
  ```bash
227
640
  # Install dependencies
228
641
  npm install
229
642
 
643
+ # Run tests
644
+ npm run test
645
+
646
+ # Run tests with coverage
647
+ npm run test:coverage
648
+
230
649
  # Build the package
231
650
  npm run build
232
651
 
233
652
  # Type checking
234
653
  npm run typecheck
235
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
+
236
664
  # Watch mode for development
237
665
  npm run dev
238
666
  ```
239
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
+
240
699
  ## License
241
700
 
242
- MIT © Oleksandr Kyrychenko
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.0",
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",
@@ -44,7 +44,7 @@
44
44
  "loading",
45
45
  "async-operations"
46
46
  ],
47
- "author": "Oleksandr Kyrychenko <your@email.com>",
47
+ "author": "Olexii Kyrychenko alexey.kirichenko@gmail.com",
48
48
  "license": "MIT",
49
49
  "repository": {
50
50
  "type": "git",