@okyrychenko-dev/react-action-guard 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Olexii Kyrychenko
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,242 @@
1
+ # @okyrychenko-dev/react-action-guard
2
+
3
+ > Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup
4
+
5
+ ## Features
6
+
7
+ - Priority-based blocking system
8
+ - Scoped blocking (global, specific areas, or multiple scopes)
9
+ - Automatic cleanup on unmount
10
+ - TypeScript support
11
+ - Built on Zustand for efficient state management
12
+ - Hooks-based API
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @okyrychenko-dev/react-action-guard zustand
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```jsx
23
+ import { useBlocker, useIsBlocked } from '@okyrychenko-dev/react-action-guard';
24
+
25
+ function MyComponent() {
26
+ const isBlocked = useIsBlocked('my-scope');
27
+
28
+ return (
29
+ <button disabled={isBlocked}>
30
+ Click me
31
+ </button>
32
+ );
33
+ }
34
+ ```
35
+
36
+ ## API Reference
37
+
38
+ ### Hooks
39
+
40
+ #### `useBlocker(blockerId, config, isActive)`
41
+
42
+ Automatically adds a blocker when the component mounts and removes it on unmount.
43
+
44
+ **Parameters:**
45
+ - `blockerId: string` - Unique identifier for the blocker
46
+ - `config: BlockerConfig` - Configuration object
47
+ - `scope?: string | string[]` - Scope(s) to block (default: "global")
48
+ - `reason?: string` - Reason for blocking (for debugging)
49
+ - `priority?: number` - Priority level (higher = more important)
50
+ - `isActive?: boolean` - Whether the blocker is active (default: true)
51
+
52
+ **Example:**
53
+
54
+ ```jsx
55
+ function MyComponent() {
56
+ useBlocker('my-blocker', {
57
+ scope: 'form',
58
+ reason: 'Form is saving',
59
+ priority: 10
60
+ });
61
+
62
+ return <div>Content</div>;
63
+ }
64
+ ```
65
+
66
+ #### `useIsBlocked(scope)`
67
+
68
+ Checks if a specific scope (or scopes) is currently blocked.
69
+
70
+ **Parameters:**
71
+ - `scope?: string | string[]` - Scope(s) to check (default: "global")
72
+
73
+ **Returns:** `boolean` - Whether the scope is blocked
74
+
75
+ **Example:**
76
+
77
+ ```jsx
78
+ function SubmitButton() {
79
+ const isFormBlocked = useIsBlocked('form');
80
+
81
+ return (
82
+ <button disabled={isFormBlocked}>
83
+ Submit
84
+ </button>
85
+ );
86
+ }
87
+ ```
88
+
89
+ #### `useAsyncAction(actionId, scope)`
90
+
91
+ Wraps an async function with automatic blocking/unblocking.
92
+
93
+ **Parameters:**
94
+ - `actionId: string` - Identifier for the action
95
+ - `scope?: string | string[]` - Scope(s) to block during execution
96
+
97
+ **Returns:** `(asyncFn: () => Promise<T>) => Promise<T>` - Function wrapper
98
+
99
+ **Example:**
100
+
101
+ ```jsx
102
+ function MyComponent() {
103
+ const executeWithBlocking = useAsyncAction('save-data', 'form');
104
+
105
+ const handleSave = async () => {
106
+ await executeWithBlocking(async () => {
107
+ await saveData();
108
+ });
109
+ };
110
+
111
+ return <button onClick={handleSave}>Save</button>;
112
+ }
113
+ ```
114
+
115
+ ### Store
116
+
117
+ #### `useUIBlockingStore`
118
+
119
+ Direct access to the Zustand store for advanced use cases.
120
+
121
+ **Methods:**
122
+ - `addBlocker(id, config)` - Manually add a blocker
123
+ - `removeBlocker(id)` - Manually remove a blocker
124
+ - `isBlocked(scope)` - Check if scope is blocked
125
+ - `getBlockingInfo(scope)` - Get detailed blocking information
126
+ - `clearAllBlockers()` - Remove all blockers
127
+ - `clearBlockersForScope(scope)` - Remove blockers for specific scope
128
+
129
+ **Example:**
130
+
131
+ ```jsx
132
+ import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
133
+
134
+ function AdvancedComponent() {
135
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
136
+
137
+ const startBlocking = () => {
138
+ addBlocker('custom-blocker', {
139
+ scope: ['form', 'navigation'],
140
+ reason: 'Critical operation in progress',
141
+ priority: 100
142
+ });
143
+ };
144
+
145
+ const stopBlocking = () => {
146
+ removeBlocker('custom-blocker');
147
+ };
148
+
149
+ return (
150
+ <div>
151
+ <button onClick={startBlocking}>Start</button>
152
+ <button onClick={stopBlocking}>Stop</button>
153
+ </div>
154
+ );
155
+ }
156
+ ```
157
+
158
+ ## TypeScript
159
+
160
+ The package is written in TypeScript and includes full type definitions.
161
+
162
+ ```typescript
163
+ import type {
164
+ BlockerConfig,
165
+ BlockerInfo,
166
+ UIBlockingState
167
+ } from '@okyrychenko-dev/react-action-guard';
168
+ ```
169
+
170
+ ## Use Cases
171
+
172
+ ### Loading States
173
+
174
+ ```jsx
175
+ function DataLoader() {
176
+ const [isLoading, setIsLoading] = useState(false);
177
+
178
+ useBlocker('data-loader', {
179
+ scope: 'content',
180
+ reason: 'Loading data'
181
+ }, isLoading);
182
+
183
+ // ... rest of component
184
+ }
185
+ ```
186
+
187
+ ### Form Submission
188
+
189
+ ```jsx
190
+ function UserForm() {
191
+ const executeWithBlocking = useAsyncAction('submit-form', 'form');
192
+ const isBlocked = useIsBlocked('form');
193
+
194
+ const handleSubmit = async (data) => {
195
+ await executeWithBlocking(async () => {
196
+ await submitForm(data);
197
+ });
198
+ };
199
+
200
+ return (
201
+ <form onSubmit={handleSubmit}>
202
+ <input disabled={isBlocked} />
203
+ <button disabled={isBlocked}>Submit</button>
204
+ </form>
205
+ );
206
+ }
207
+ ```
208
+
209
+ ### Global Loading Overlay
210
+
211
+ ```jsx
212
+ function App() {
213
+ const isGloballyBlocked = useIsBlocked('global');
214
+
215
+ return (
216
+ <div>
217
+ {isGloballyBlocked && <LoadingOverlay />}
218
+ <YourApp />
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ # Install dependencies
228
+ npm install
229
+
230
+ # Build the package
231
+ npm run build
232
+
233
+ # Type checking
234
+ npm run typecheck
235
+
236
+ # Watch mode for development
237
+ npm run dev
238
+ ```
239
+
240
+ ## License
241
+
242
+ MIT © Oleksandr Kyrychenko
package/dist/index.cjs ADDED
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var zustand = require('zustand');
5
+ var middleware = require('zustand/middleware');
6
+
7
+ // src/hooks/useBlocker.ts
8
+
9
+ // src/store/uiBlockingStore.constants.ts
10
+ var DEFAULT_SCOPE = "global";
11
+ var DEFAULT_REASON = "Unknown";
12
+ var DEFAULT_PRIORITY = 0;
13
+ var ASYNC_ACTION_PRIORITY = 20;
14
+ var DEVTOOLS_NAME = "UIBlocking";
15
+
16
+ // src/store/uiBlockingStore.utils.ts
17
+ function normalizeScopeToArray(scope) {
18
+ if (typeof scope === "string") {
19
+ return [scope];
20
+ }
21
+ return Array.from(scope);
22
+ }
23
+
24
+ // src/store/uiBlockingStore.actions.ts
25
+ var createUIBlockingActions = (set, get) => ({
26
+ // State
27
+ activeBlockers: /* @__PURE__ */ new Map(),
28
+ // Actions
29
+ /**
30
+ * Add a blocker to the store
31
+ *
32
+ * @param id - Unique blocker identifier
33
+ * @param config - Optional blocker configuration
34
+ *
35
+ */
36
+ addBlocker: (id, config = {}) => {
37
+ set((state) => {
38
+ const newBlockers = new Map(state.activeBlockers);
39
+ const storedBlocker = {
40
+ scope: config.scope ?? DEFAULT_SCOPE,
41
+ reason: config.reason ?? DEFAULT_REASON,
42
+ priority: config.priority ?? DEFAULT_PRIORITY,
43
+ timestamp: config.timestamp ?? Date.now()
44
+ };
45
+ newBlockers.set(id, storedBlocker);
46
+ return { activeBlockers: newBlockers };
47
+ });
48
+ },
49
+ /**
50
+ * Remove a blocker from the store
51
+ *
52
+ * @param id - Blocker identifier to remove
53
+ *
54
+ */
55
+ removeBlocker: (id) => {
56
+ set((state) => {
57
+ const newBlockers = new Map(state.activeBlockers);
58
+ newBlockers.delete(id);
59
+ return { activeBlockers: newBlockers };
60
+ });
61
+ },
62
+ /**
63
+ * Check if a scope is currently blocked
64
+ *
65
+ * @param scope - Scope or array of scopes to check (default: "global")
66
+ * @returns True if any of the scopes are blocked
67
+ *
68
+ */
69
+ isBlocked: (scope = DEFAULT_SCOPE) => {
70
+ const { activeBlockers } = get();
71
+ const scopes = normalizeScopeToArray(scope);
72
+ for (const [, blocker] of activeBlockers) {
73
+ if (blocker.scope === DEFAULT_SCOPE) {
74
+ return true;
75
+ }
76
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
77
+ if (scopes.some((s) => blockerScopes.includes(s))) {
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ },
83
+ /**
84
+ * Get detailed blocking information for a specific scope
85
+ *
86
+ * @param scope - Scope to get blocking information for
87
+ * @returns Array of blockers affecting the scope, sorted by priority (highest first)
88
+ *
89
+ */
90
+ getBlockingInfo: (scope) => {
91
+ const { activeBlockers } = get();
92
+ const blockers = [];
93
+ for (const [id, blocker] of activeBlockers) {
94
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
95
+ if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
96
+ blockers.push({ id, ...blocker });
97
+ }
98
+ }
99
+ return blockers.sort((a, b) => b.priority - a.priority);
100
+ },
101
+ /**
102
+ * Clear all blockers from the store
103
+ */
104
+ clearAllBlockers: () => {
105
+ set({ activeBlockers: /* @__PURE__ */ new Map() });
106
+ },
107
+ /**
108
+ * Clear all blockers for a specific scope
109
+ *
110
+ * @param scope - Scope to clear blockers for
111
+ *
112
+ */
113
+ clearBlockersForScope: (scope) => {
114
+ set((state) => {
115
+ const newBlockers = /* @__PURE__ */ new Map();
116
+ for (const [id, blocker] of state.activeBlockers) {
117
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
118
+ if (!blockerScopes.includes(scope)) {
119
+ newBlockers.set(id, blocker);
120
+ }
121
+ }
122
+ return { activeBlockers: newBlockers };
123
+ });
124
+ }
125
+ });
126
+
127
+ // src/store/uiBlockingStore.config.ts
128
+ var devtoolsConfig = {
129
+ name: DEVTOOLS_NAME,
130
+ enabled: process.env.NODE_ENV !== "production"
131
+ };
132
+
133
+ // src/store/uiBlockingStore.store.ts
134
+ var useUIBlockingStore = zustand.create()(
135
+ middleware.devtools(createUIBlockingActions, devtoolsConfig)
136
+ );
137
+
138
+ // src/hooks/useBlocker.ts
139
+ var useBlocker = (blockerId, config, isActive = true) => {
140
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
141
+ react.useEffect(() => {
142
+ if (!isActive || !blockerId) {
143
+ return;
144
+ }
145
+ addBlocker(blockerId, config);
146
+ return () => {
147
+ removeBlocker(blockerId);
148
+ };
149
+ }, [blockerId, isActive, addBlocker, removeBlocker]);
150
+ };
151
+
152
+ // src/hooks/useIsBlocked.ts
153
+ var useIsBlocked = (scope) => {
154
+ return useUIBlockingStore((state) => state.isBlocked(scope));
155
+ };
156
+ var useAsyncAction = (actionId, scope) => {
157
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
158
+ const executeWithBlocking = react.useCallback(
159
+ async (asyncFn) => {
160
+ const blockerId = `${actionId}-${Date.now().toString()}`;
161
+ try {
162
+ addBlocker(blockerId, {
163
+ scope,
164
+ reason: `Executing ${actionId}`,
165
+ priority: ASYNC_ACTION_PRIORITY
166
+ });
167
+ return await asyncFn();
168
+ } finally {
169
+ removeBlocker(blockerId);
170
+ }
171
+ },
172
+ [actionId, scope, addBlocker, removeBlocker]
173
+ );
174
+ return executeWithBlocking;
175
+ };
176
+
177
+ exports.useAsyncAction = useAsyncAction;
178
+ exports.useBlocker = useBlocker;
179
+ exports.useIsBlocked = useIsBlocked;
180
+ exports.useUIBlockingStore = useUIBlockingStore;
181
+ //# sourceMappingURL=index.cjs.map
182
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/uiBlockingStore.constants.ts","../src/store/uiBlockingStore.utils.ts","../src/store/uiBlockingStore.actions.ts","../src/store/uiBlockingStore.config.ts","../src/store/uiBlockingStore.store.ts","../src/hooks/useBlocker.ts","../src/hooks/useIsBlocked.ts","../src/hooks/useAsyncAction.ts"],"names":["create","devtools","useEffect","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Default values and constants for UI Blocking Store\n */\n\n/**\n * Default scope for blockers\n */\nexport const DEFAULT_SCOPE = \"global\" as const;\n\n/**\n * Default reason when none is provided\n */\nexport const DEFAULT_REASON = \"Unknown\" as const;\n\n/**\n * Default priority for blockers\n */\nexport const DEFAULT_PRIORITY = 0 as const;\n\n/**\n * Default priority for async actions\n */\nexport const ASYNC_ACTION_PRIORITY = 20 as const;\n\n/**\n * DevTools store name\n */\nexport const DEVTOOLS_NAME = \"UIBlocking\" as const;\n","/**\n * Normalize scope to array format\n *\n * @param scope - Single scope or array of scopes\n * @returns Array of scopes\n *\n * @internal\n */\nexport function normalizeScopeToArray(scope: string | ReadonlyArray<string>): Array<string> {\n if (typeof scope === \"string\") {\n return [scope];\n }\n return Array.from(scope);\n}\n","import type { StateCreator } from \"zustand\";\nimport type {\n BlockerConfig,\n BlockerInfo,\n StoredBlocker,\n UIBlockingStore,\n} from \"./uiBlockingStore.types\";\nimport { DEFAULT_SCOPE, DEFAULT_REASON, DEFAULT_PRIORITY } from \"./uiBlockingStore.constants\";\nimport { normalizeScopeToArray } from \"./uiBlockingStore.utils\";\n\n/**\n * UI Blocking Store Slice\n *\n * Implements the state and actions for UI blocking management.\n * This slice follows the Zustand slice pattern for better code organization.\n */\nexport const createUIBlockingActions: StateCreator<UIBlockingStore, [], [], UIBlockingStore> = (\n set,\n get\n) => ({\n // State\n activeBlockers: new Map(),\n\n // Actions\n /**\n * Add a blocker to the store\n *\n * @param id - Unique blocker identifier\n * @param config - Optional blocker configuration\n *\n */\n addBlocker: (id: string, config: BlockerConfig = {}): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n const storedBlocker: StoredBlocker = {\n scope: config.scope ?? DEFAULT_SCOPE,\n reason: config.reason ?? DEFAULT_REASON,\n priority: config.priority ?? DEFAULT_PRIORITY,\n timestamp: config.timestamp ?? Date.now(),\n };\n newBlockers.set(id, storedBlocker);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Remove a blocker from the store\n *\n * @param id - Blocker identifier to remove\n *\n */\n removeBlocker: (id: string): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n newBlockers.delete(id);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Check if a scope is currently blocked\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the scopes are blocked\n *\n */\n isBlocked: (scope: string | ReadonlyArray<string> = DEFAULT_SCOPE): boolean => {\n const { activeBlockers } = get();\n const scopes = normalizeScopeToArray(scope);\n\n for (const [, blocker] of activeBlockers) {\n // Global blocking blocks everything\n if (blocker.scope === DEFAULT_SCOPE) {\n return true;\n }\n\n // Check scope intersection\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (scopes.some((s) => blockerScopes.includes(s))) {\n return true;\n }\n }\n\n return false;\n },\n\n /**\n * Get detailed blocking information for a specific scope\n *\n * @param scope - Scope to get blocking information for\n * @returns Array of blockers affecting the scope, sorted by priority (highest first)\n *\n */\n getBlockingInfo: (scope: string): ReadonlyArray<BlockerInfo> => {\n const { activeBlockers } = get();\n const blockers: Array<BlockerInfo> = [];\n\n for (const [id, blocker] of activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {\n blockers.push({ id, ...blocker });\n }\n }\n\n return blockers.sort((a, b) => b.priority - a.priority);\n },\n\n /**\n * Clear all blockers from the store\n */\n clearAllBlockers: (): void => {\n set({ activeBlockers: new Map() });\n },\n\n /**\n * Clear all blockers for a specific scope\n *\n * @param scope - Scope to clear blockers for\n *\n */\n clearBlockersForScope: (scope: string): void => {\n set((state) => {\n const newBlockers = new Map();\n\n for (const [id, blocker] of state.activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (!blockerScopes.includes(scope)) {\n newBlockers.set(id, blocker);\n }\n }\n\n return { activeBlockers: newBlockers };\n });\n },\n});\n","import type { DevtoolsOptions } from \"zustand/middleware\";\nimport { DEVTOOLS_NAME } from \"./uiBlockingStore.constants\";\n\n/**\n * DevTools configuration for UI Blocking Store\n *\n * Enables Redux DevTools integration only in development mode\n * for better debugging experience without production overhead.\n */\nexport const devtoolsConfig: DevtoolsOptions = {\n name: DEVTOOLS_NAME,\n enabled: process.env.NODE_ENV !== \"production\",\n};\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { createUIBlockingActions } from \"./uiBlockingStore.actions\";\nimport { devtoolsConfig } from \"./uiBlockingStore.config\";\nimport type { UIBlockingStore } from \"./uiBlockingStore.types\";\n\n/**\n * UI Blocking Store\n *\n * Global Zustand store for managing UI blocking state across the application.\n *\n * Features:\n * - Priority-based blocking system\n * - Scope-based blocking (global, specific areas, or multiple scopes)\n * - Automatic cleanup on unmount (when using hooks)\n * - DevTools integration for debugging\n *\n */\nexport const useUIBlockingStore = create<UIBlockingStore>()(\n devtools(createUIBlockingActions, devtoolsConfig)\n);\n","import { useEffect } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport type { BlockerConfig } from \"../store\";\n\n/**\n * Hook to automatically add and remove a blocker based on component lifecycle\n *\n * Automatically adds a blocker when the component mounts (or when activated)\n * and removes it when the component unmounts (or when deactivated).\n *\n * @param blockerId - Unique identifier for the blocker\n * @param config - Blocker configuration (scope, reason, priority)\n * @param isActive - Whether the blocker is currently active (default: true)\n *\n */\nexport const useBlocker = (blockerId: string, config: BlockerConfig, isActive = true): void => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n useEffect(() => {\n if (!isActive || !blockerId) {\n return;\n }\n\n addBlocker(blockerId, config);\n\n return () => {\n removeBlocker(blockerId);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [blockerId, isActive, addBlocker, removeBlocker]);\n};\n","import { useUIBlockingStore } from \"../store\";\n\n/**\n * Hook to check if a scope is currently blocked\n *\n * Returns a boolean indicating whether the specified scope(s) are blocked.\n * Automatically re-renders when the blocking state changes.\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the specified scopes are blocked\n *\n */\nexport const useIsBlocked = (scope?: string | ReadonlyArray<string>): boolean => {\n return useUIBlockingStore((state) => state.isBlocked(scope));\n};\n","import { useCallback } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport { ASYNC_ACTION_PRIORITY } from \"../store\";\n\n/**\n * Hook to wrap async functions with automatic blocking/unblocking\n *\n * Creates a wrapper function that automatically adds a blocker before executing\n * the async function and removes it after completion (success or failure).\n *\n * @template T - The return type of the async function\n * @param actionId - Unique identifier for the action (used in blocker ID and reason)\n * @param scope - Scope(s) to block during execution (default: \"global\")\n * @returns Function that wraps async functions with blocking behavior\n *\n */\nexport const useAsyncAction = <T = unknown>(\n actionId: string,\n scope?: string | ReadonlyArray<string>\n): ((asyncFn: () => Promise<T>) => Promise<T>) => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n const executeWithBlocking = useCallback(\n async (asyncFn: () => Promise<T>): Promise<T> => {\n const blockerId = `${actionId}-${Date.now().toString()}`;\n\n try {\n addBlocker(blockerId, {\n scope,\n reason: `Executing ${actionId}`,\n priority: ASYNC_ACTION_PRIORITY,\n });\n\n return await asyncFn();\n } finally {\n removeBlocker(blockerId);\n }\n },\n [actionId, scope, addBlocker, removeBlocker]\n );\n\n return executeWithBlocking;\n};\n"]}
@@ -0,0 +1,118 @@
1
+ import * as zustand from 'zustand';
2
+
3
+ /**
4
+ * Configuration for a blocker
5
+ */
6
+ interface BlockerConfig {
7
+ scope?: string | ReadonlyArray<string>;
8
+ reason?: string;
9
+ priority?: number;
10
+ timestamp?: number;
11
+ }
12
+ /**
13
+ * Stored blocker with all required fields
14
+ */
15
+ interface StoredBlocker {
16
+ scope: string | ReadonlyArray<string>;
17
+ reason: string;
18
+ priority: number;
19
+ timestamp: number;
20
+ }
21
+ /**
22
+ * Blocker information including its ID
23
+ */
24
+ interface BlockerInfo extends StoredBlocker {
25
+ id: string;
26
+ }
27
+ /**
28
+ * Internal store state
29
+ */
30
+ interface UIBlockingStoreState {
31
+ activeBlockers: Map<string, StoredBlocker>;
32
+ }
33
+ /**
34
+ * Store actions
35
+ */
36
+ interface UIBlockingStoreActions {
37
+ addBlocker: (id: string, config?: BlockerConfig) => void;
38
+ removeBlocker: (id: string) => void;
39
+ isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
40
+ getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
41
+ clearAllBlockers: () => void;
42
+ clearBlockersForScope: (scope: string) => void;
43
+ }
44
+ /**
45
+ * Complete store type (state + actions)
46
+ */
47
+ type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
48
+
49
+ /**
50
+ * UI Blocking Store
51
+ *
52
+ * Global Zustand store for managing UI blocking state across the application.
53
+ *
54
+ * Features:
55
+ * - Priority-based blocking system
56
+ * - Scope-based blocking (global, specific areas, or multiple scopes)
57
+ * - Automatic cleanup on unmount (when using hooks)
58
+ * - DevTools integration for debugging
59
+ *
60
+ */
61
+ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
62
+ setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
63
+ [x: string]: unknown;
64
+ [x: number]: unknown;
65
+ [x: symbol]: unknown;
66
+ type: string;
67
+ }) | undefined): void;
68
+ setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
69
+ [x: string]: unknown;
70
+ [x: number]: unknown;
71
+ [x: symbol]: unknown;
72
+ type: string;
73
+ }) | undefined): void;
74
+ devtools: {
75
+ cleanup: () => void;
76
+ };
77
+ }>;
78
+
79
+ /**
80
+ * Hook to automatically add and remove a blocker based on component lifecycle
81
+ *
82
+ * Automatically adds a blocker when the component mounts (or when activated)
83
+ * and removes it when the component unmounts (or when deactivated).
84
+ *
85
+ * @param blockerId - Unique identifier for the blocker
86
+ * @param config - Blocker configuration (scope, reason, priority)
87
+ * @param isActive - Whether the blocker is currently active (default: true)
88
+ *
89
+ */
90
+ declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
91
+
92
+ /**
93
+ * Hook to check if a scope is currently blocked
94
+ *
95
+ * Returns a boolean indicating whether the specified scope(s) are blocked.
96
+ * Automatically re-renders when the blocking state changes.
97
+ *
98
+ * @param scope - Scope or array of scopes to check (default: "global")
99
+ * @returns True if any of the specified scopes are blocked
100
+ *
101
+ */
102
+ declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
103
+
104
+ /**
105
+ * Hook to wrap async functions with automatic blocking/unblocking
106
+ *
107
+ * Creates a wrapper function that automatically adds a blocker before executing
108
+ * the async function and removes it after completion (success or failure).
109
+ *
110
+ * @template T - The return type of the async function
111
+ * @param actionId - Unique identifier for the action (used in blocker ID and reason)
112
+ * @param scope - Scope(s) to block during execution (default: "global")
113
+ * @returns Function that wraps async functions with blocking behavior
114
+ *
115
+ */
116
+ declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
117
+
118
+ export { type BlockerConfig, type BlockerInfo, type StoredBlocker, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
@@ -0,0 +1,118 @@
1
+ import * as zustand from 'zustand';
2
+
3
+ /**
4
+ * Configuration for a blocker
5
+ */
6
+ interface BlockerConfig {
7
+ scope?: string | ReadonlyArray<string>;
8
+ reason?: string;
9
+ priority?: number;
10
+ timestamp?: number;
11
+ }
12
+ /**
13
+ * Stored blocker with all required fields
14
+ */
15
+ interface StoredBlocker {
16
+ scope: string | ReadonlyArray<string>;
17
+ reason: string;
18
+ priority: number;
19
+ timestamp: number;
20
+ }
21
+ /**
22
+ * Blocker information including its ID
23
+ */
24
+ interface BlockerInfo extends StoredBlocker {
25
+ id: string;
26
+ }
27
+ /**
28
+ * Internal store state
29
+ */
30
+ interface UIBlockingStoreState {
31
+ activeBlockers: Map<string, StoredBlocker>;
32
+ }
33
+ /**
34
+ * Store actions
35
+ */
36
+ interface UIBlockingStoreActions {
37
+ addBlocker: (id: string, config?: BlockerConfig) => void;
38
+ removeBlocker: (id: string) => void;
39
+ isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
40
+ getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
41
+ clearAllBlockers: () => void;
42
+ clearBlockersForScope: (scope: string) => void;
43
+ }
44
+ /**
45
+ * Complete store type (state + actions)
46
+ */
47
+ type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
48
+
49
+ /**
50
+ * UI Blocking Store
51
+ *
52
+ * Global Zustand store for managing UI blocking state across the application.
53
+ *
54
+ * Features:
55
+ * - Priority-based blocking system
56
+ * - Scope-based blocking (global, specific areas, or multiple scopes)
57
+ * - Automatic cleanup on unmount (when using hooks)
58
+ * - DevTools integration for debugging
59
+ *
60
+ */
61
+ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
62
+ setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
63
+ [x: string]: unknown;
64
+ [x: number]: unknown;
65
+ [x: symbol]: unknown;
66
+ type: string;
67
+ }) | undefined): void;
68
+ setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
69
+ [x: string]: unknown;
70
+ [x: number]: unknown;
71
+ [x: symbol]: unknown;
72
+ type: string;
73
+ }) | undefined): void;
74
+ devtools: {
75
+ cleanup: () => void;
76
+ };
77
+ }>;
78
+
79
+ /**
80
+ * Hook to automatically add and remove a blocker based on component lifecycle
81
+ *
82
+ * Automatically adds a blocker when the component mounts (or when activated)
83
+ * and removes it when the component unmounts (or when deactivated).
84
+ *
85
+ * @param blockerId - Unique identifier for the blocker
86
+ * @param config - Blocker configuration (scope, reason, priority)
87
+ * @param isActive - Whether the blocker is currently active (default: true)
88
+ *
89
+ */
90
+ declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
91
+
92
+ /**
93
+ * Hook to check if a scope is currently blocked
94
+ *
95
+ * Returns a boolean indicating whether the specified scope(s) are blocked.
96
+ * Automatically re-renders when the blocking state changes.
97
+ *
98
+ * @param scope - Scope or array of scopes to check (default: "global")
99
+ * @returns True if any of the specified scopes are blocked
100
+ *
101
+ */
102
+ declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
103
+
104
+ /**
105
+ * Hook to wrap async functions with automatic blocking/unblocking
106
+ *
107
+ * Creates a wrapper function that automatically adds a blocker before executing
108
+ * the async function and removes it after completion (success or failure).
109
+ *
110
+ * @template T - The return type of the async function
111
+ * @param actionId - Unique identifier for the action (used in blocker ID and reason)
112
+ * @param scope - Scope(s) to block during execution (default: "global")
113
+ * @returns Function that wraps async functions with blocking behavior
114
+ *
115
+ */
116
+ declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
117
+
118
+ export { type BlockerConfig, type BlockerInfo, type StoredBlocker, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ import { useEffect, useCallback } from 'react';
2
+ import { create } from 'zustand';
3
+ import { devtools } from 'zustand/middleware';
4
+
5
+ // src/hooks/useBlocker.ts
6
+
7
+ // src/store/uiBlockingStore.constants.ts
8
+ var DEFAULT_SCOPE = "global";
9
+ var DEFAULT_REASON = "Unknown";
10
+ var DEFAULT_PRIORITY = 0;
11
+ var ASYNC_ACTION_PRIORITY = 20;
12
+ var DEVTOOLS_NAME = "UIBlocking";
13
+
14
+ // src/store/uiBlockingStore.utils.ts
15
+ function normalizeScopeToArray(scope) {
16
+ if (typeof scope === "string") {
17
+ return [scope];
18
+ }
19
+ return Array.from(scope);
20
+ }
21
+
22
+ // src/store/uiBlockingStore.actions.ts
23
+ var createUIBlockingActions = (set, get) => ({
24
+ // State
25
+ activeBlockers: /* @__PURE__ */ new Map(),
26
+ // Actions
27
+ /**
28
+ * Add a blocker to the store
29
+ *
30
+ * @param id - Unique blocker identifier
31
+ * @param config - Optional blocker configuration
32
+ *
33
+ */
34
+ addBlocker: (id, config = {}) => {
35
+ set((state) => {
36
+ const newBlockers = new Map(state.activeBlockers);
37
+ const storedBlocker = {
38
+ scope: config.scope ?? DEFAULT_SCOPE,
39
+ reason: config.reason ?? DEFAULT_REASON,
40
+ priority: config.priority ?? DEFAULT_PRIORITY,
41
+ timestamp: config.timestamp ?? Date.now()
42
+ };
43
+ newBlockers.set(id, storedBlocker);
44
+ return { activeBlockers: newBlockers };
45
+ });
46
+ },
47
+ /**
48
+ * Remove a blocker from the store
49
+ *
50
+ * @param id - Blocker identifier to remove
51
+ *
52
+ */
53
+ removeBlocker: (id) => {
54
+ set((state) => {
55
+ const newBlockers = new Map(state.activeBlockers);
56
+ newBlockers.delete(id);
57
+ return { activeBlockers: newBlockers };
58
+ });
59
+ },
60
+ /**
61
+ * Check if a scope is currently blocked
62
+ *
63
+ * @param scope - Scope or array of scopes to check (default: "global")
64
+ * @returns True if any of the scopes are blocked
65
+ *
66
+ */
67
+ isBlocked: (scope = DEFAULT_SCOPE) => {
68
+ const { activeBlockers } = get();
69
+ const scopes = normalizeScopeToArray(scope);
70
+ for (const [, blocker] of activeBlockers) {
71
+ if (blocker.scope === DEFAULT_SCOPE) {
72
+ return true;
73
+ }
74
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
75
+ if (scopes.some((s) => blockerScopes.includes(s))) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ },
81
+ /**
82
+ * Get detailed blocking information for a specific scope
83
+ *
84
+ * @param scope - Scope to get blocking information for
85
+ * @returns Array of blockers affecting the scope, sorted by priority (highest first)
86
+ *
87
+ */
88
+ getBlockingInfo: (scope) => {
89
+ const { activeBlockers } = get();
90
+ const blockers = [];
91
+ for (const [id, blocker] of activeBlockers) {
92
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
93
+ if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
94
+ blockers.push({ id, ...blocker });
95
+ }
96
+ }
97
+ return blockers.sort((a, b) => b.priority - a.priority);
98
+ },
99
+ /**
100
+ * Clear all blockers from the store
101
+ */
102
+ clearAllBlockers: () => {
103
+ set({ activeBlockers: /* @__PURE__ */ new Map() });
104
+ },
105
+ /**
106
+ * Clear all blockers for a specific scope
107
+ *
108
+ * @param scope - Scope to clear blockers for
109
+ *
110
+ */
111
+ clearBlockersForScope: (scope) => {
112
+ set((state) => {
113
+ const newBlockers = /* @__PURE__ */ new Map();
114
+ for (const [id, blocker] of state.activeBlockers) {
115
+ const blockerScopes = normalizeScopeToArray(blocker.scope);
116
+ if (!blockerScopes.includes(scope)) {
117
+ newBlockers.set(id, blocker);
118
+ }
119
+ }
120
+ return { activeBlockers: newBlockers };
121
+ });
122
+ }
123
+ });
124
+
125
+ // src/store/uiBlockingStore.config.ts
126
+ var devtoolsConfig = {
127
+ name: DEVTOOLS_NAME,
128
+ enabled: process.env.NODE_ENV !== "production"
129
+ };
130
+
131
+ // src/store/uiBlockingStore.store.ts
132
+ var useUIBlockingStore = create()(
133
+ devtools(createUIBlockingActions, devtoolsConfig)
134
+ );
135
+
136
+ // src/hooks/useBlocker.ts
137
+ var useBlocker = (blockerId, config, isActive = true) => {
138
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
139
+ useEffect(() => {
140
+ if (!isActive || !blockerId) {
141
+ return;
142
+ }
143
+ addBlocker(blockerId, config);
144
+ return () => {
145
+ removeBlocker(blockerId);
146
+ };
147
+ }, [blockerId, isActive, addBlocker, removeBlocker]);
148
+ };
149
+
150
+ // src/hooks/useIsBlocked.ts
151
+ var useIsBlocked = (scope) => {
152
+ return useUIBlockingStore((state) => state.isBlocked(scope));
153
+ };
154
+ var useAsyncAction = (actionId, scope) => {
155
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
156
+ const executeWithBlocking = useCallback(
157
+ async (asyncFn) => {
158
+ const blockerId = `${actionId}-${Date.now().toString()}`;
159
+ try {
160
+ addBlocker(blockerId, {
161
+ scope,
162
+ reason: `Executing ${actionId}`,
163
+ priority: ASYNC_ACTION_PRIORITY
164
+ });
165
+ return await asyncFn();
166
+ } finally {
167
+ removeBlocker(blockerId);
168
+ }
169
+ },
170
+ [actionId, scope, addBlocker, removeBlocker]
171
+ );
172
+ return executeWithBlocking;
173
+ };
174
+
175
+ export { useAsyncAction, useBlocker, useIsBlocked, useUIBlockingStore };
176
+ //# sourceMappingURL=index.js.map
177
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/uiBlockingStore.constants.ts","../src/store/uiBlockingStore.utils.ts","../src/store/uiBlockingStore.actions.ts","../src/store/uiBlockingStore.config.ts","../src/store/uiBlockingStore.store.ts","../src/hooks/useBlocker.ts","../src/hooks/useIsBlocked.ts","../src/hooks/useAsyncAction.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Default values and constants for UI Blocking Store\n */\n\n/**\n * Default scope for blockers\n */\nexport const DEFAULT_SCOPE = \"global\" as const;\n\n/**\n * Default reason when none is provided\n */\nexport const DEFAULT_REASON = \"Unknown\" as const;\n\n/**\n * Default priority for blockers\n */\nexport const DEFAULT_PRIORITY = 0 as const;\n\n/**\n * Default priority for async actions\n */\nexport const ASYNC_ACTION_PRIORITY = 20 as const;\n\n/**\n * DevTools store name\n */\nexport const DEVTOOLS_NAME = \"UIBlocking\" as const;\n","/**\n * Normalize scope to array format\n *\n * @param scope - Single scope or array of scopes\n * @returns Array of scopes\n *\n * @internal\n */\nexport function normalizeScopeToArray(scope: string | ReadonlyArray<string>): Array<string> {\n if (typeof scope === \"string\") {\n return [scope];\n }\n return Array.from(scope);\n}\n","import type { StateCreator } from \"zustand\";\nimport type {\n BlockerConfig,\n BlockerInfo,\n StoredBlocker,\n UIBlockingStore,\n} from \"./uiBlockingStore.types\";\nimport { DEFAULT_SCOPE, DEFAULT_REASON, DEFAULT_PRIORITY } from \"./uiBlockingStore.constants\";\nimport { normalizeScopeToArray } from \"./uiBlockingStore.utils\";\n\n/**\n * UI Blocking Store Slice\n *\n * Implements the state and actions for UI blocking management.\n * This slice follows the Zustand slice pattern for better code organization.\n */\nexport const createUIBlockingActions: StateCreator<UIBlockingStore, [], [], UIBlockingStore> = (\n set,\n get\n) => ({\n // State\n activeBlockers: new Map(),\n\n // Actions\n /**\n * Add a blocker to the store\n *\n * @param id - Unique blocker identifier\n * @param config - Optional blocker configuration\n *\n */\n addBlocker: (id: string, config: BlockerConfig = {}): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n const storedBlocker: StoredBlocker = {\n scope: config.scope ?? DEFAULT_SCOPE,\n reason: config.reason ?? DEFAULT_REASON,\n priority: config.priority ?? DEFAULT_PRIORITY,\n timestamp: config.timestamp ?? Date.now(),\n };\n newBlockers.set(id, storedBlocker);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Remove a blocker from the store\n *\n * @param id - Blocker identifier to remove\n *\n */\n removeBlocker: (id: string): void => {\n set((state) => {\n const newBlockers = new Map(state.activeBlockers);\n newBlockers.delete(id);\n\n return { activeBlockers: newBlockers };\n });\n },\n\n /**\n * Check if a scope is currently blocked\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the scopes are blocked\n *\n */\n isBlocked: (scope: string | ReadonlyArray<string> = DEFAULT_SCOPE): boolean => {\n const { activeBlockers } = get();\n const scopes = normalizeScopeToArray(scope);\n\n for (const [, blocker] of activeBlockers) {\n // Global blocking blocks everything\n if (blocker.scope === DEFAULT_SCOPE) {\n return true;\n }\n\n // Check scope intersection\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (scopes.some((s) => blockerScopes.includes(s))) {\n return true;\n }\n }\n\n return false;\n },\n\n /**\n * Get detailed blocking information for a specific scope\n *\n * @param scope - Scope to get blocking information for\n * @returns Array of blockers affecting the scope, sorted by priority (highest first)\n *\n */\n getBlockingInfo: (scope: string): ReadonlyArray<BlockerInfo> => {\n const { activeBlockers } = get();\n const blockers: Array<BlockerInfo> = [];\n\n for (const [id, blocker] of activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {\n blockers.push({ id, ...blocker });\n }\n }\n\n return blockers.sort((a, b) => b.priority - a.priority);\n },\n\n /**\n * Clear all blockers from the store\n */\n clearAllBlockers: (): void => {\n set({ activeBlockers: new Map() });\n },\n\n /**\n * Clear all blockers for a specific scope\n *\n * @param scope - Scope to clear blockers for\n *\n */\n clearBlockersForScope: (scope: string): void => {\n set((state) => {\n const newBlockers = new Map();\n\n for (const [id, blocker] of state.activeBlockers) {\n const blockerScopes = normalizeScopeToArray(blocker.scope);\n\n if (!blockerScopes.includes(scope)) {\n newBlockers.set(id, blocker);\n }\n }\n\n return { activeBlockers: newBlockers };\n });\n },\n});\n","import type { DevtoolsOptions } from \"zustand/middleware\";\nimport { DEVTOOLS_NAME } from \"./uiBlockingStore.constants\";\n\n/**\n * DevTools configuration for UI Blocking Store\n *\n * Enables Redux DevTools integration only in development mode\n * for better debugging experience without production overhead.\n */\nexport const devtoolsConfig: DevtoolsOptions = {\n name: DEVTOOLS_NAME,\n enabled: process.env.NODE_ENV !== \"production\",\n};\n","import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { createUIBlockingActions } from \"./uiBlockingStore.actions\";\nimport { devtoolsConfig } from \"./uiBlockingStore.config\";\nimport type { UIBlockingStore } from \"./uiBlockingStore.types\";\n\n/**\n * UI Blocking Store\n *\n * Global Zustand store for managing UI blocking state across the application.\n *\n * Features:\n * - Priority-based blocking system\n * - Scope-based blocking (global, specific areas, or multiple scopes)\n * - Automatic cleanup on unmount (when using hooks)\n * - DevTools integration for debugging\n *\n */\nexport const useUIBlockingStore = create<UIBlockingStore>()(\n devtools(createUIBlockingActions, devtoolsConfig)\n);\n","import { useEffect } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport type { BlockerConfig } from \"../store\";\n\n/**\n * Hook to automatically add and remove a blocker based on component lifecycle\n *\n * Automatically adds a blocker when the component mounts (or when activated)\n * and removes it when the component unmounts (or when deactivated).\n *\n * @param blockerId - Unique identifier for the blocker\n * @param config - Blocker configuration (scope, reason, priority)\n * @param isActive - Whether the blocker is currently active (default: true)\n *\n */\nexport const useBlocker = (blockerId: string, config: BlockerConfig, isActive = true): void => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n useEffect(() => {\n if (!isActive || !blockerId) {\n return;\n }\n\n addBlocker(blockerId, config);\n\n return () => {\n removeBlocker(blockerId);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [blockerId, isActive, addBlocker, removeBlocker]);\n};\n","import { useUIBlockingStore } from \"../store\";\n\n/**\n * Hook to check if a scope is currently blocked\n *\n * Returns a boolean indicating whether the specified scope(s) are blocked.\n * Automatically re-renders when the blocking state changes.\n *\n * @param scope - Scope or array of scopes to check (default: \"global\")\n * @returns True if any of the specified scopes are blocked\n *\n */\nexport const useIsBlocked = (scope?: string | ReadonlyArray<string>): boolean => {\n return useUIBlockingStore((state) => state.isBlocked(scope));\n};\n","import { useCallback } from \"react\";\nimport { useUIBlockingStore } from \"../store\";\nimport { ASYNC_ACTION_PRIORITY } from \"../store\";\n\n/**\n * Hook to wrap async functions with automatic blocking/unblocking\n *\n * Creates a wrapper function that automatically adds a blocker before executing\n * the async function and removes it after completion (success or failure).\n *\n * @template T - The return type of the async function\n * @param actionId - Unique identifier for the action (used in blocker ID and reason)\n * @param scope - Scope(s) to block during execution (default: \"global\")\n * @returns Function that wraps async functions with blocking behavior\n *\n */\nexport const useAsyncAction = <T = unknown>(\n actionId: string,\n scope?: string | ReadonlyArray<string>\n): ((asyncFn: () => Promise<T>) => Promise<T>) => {\n const { addBlocker, removeBlocker } = useUIBlockingStore();\n\n const executeWithBlocking = useCallback(\n async (asyncFn: () => Promise<T>): Promise<T> => {\n const blockerId = `${actionId}-${Date.now().toString()}`;\n\n try {\n addBlocker(blockerId, {\n scope,\n reason: `Executing ${actionId}`,\n priority: ASYNC_ACTION_PRIORITY,\n });\n\n return await asyncFn();\n } finally {\n removeBlocker(blockerId);\n }\n },\n [actionId, scope, addBlocker, removeBlocker]\n );\n\n return executeWithBlocking;\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@okyrychenko-dev/react-action-guard",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Elegant UI blocking management for React applications with priorities, scopes, and automatic cleanup",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "clean": "rm -rf dist",
25
+ "prepublishOnly": "npm run clean && npm run build",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint src --ext .ts,.tsx",
28
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
29
+ "format": "prettier --write \"src/**/*.{ts,tsx}\"",
30
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
31
+ "test": "vitest",
32
+ "test:ui": "vitest --ui",
33
+ "test:run": "vitest run",
34
+ "test:coverage": "vitest run --coverage",
35
+ "prepare": "husky"
36
+ },
37
+ "keywords": [
38
+ "react",
39
+ "ui-blocking",
40
+ "state-management",
41
+ "zustand",
42
+ "typescript",
43
+ "loading",
44
+ "async-operations"
45
+ ],
46
+ "author": "Oleksandr Kyrychenko <your@email.com>",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/okyrychenko-dev/react-action-guard"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/okyrychenko-dev/react-action-guard/issues"
54
+ },
55
+ "homepage": "https://github.com/okyrychenko-dev/react-action-guard#readme",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^17.0.0 || ^18.0.0",
61
+ "zustand": "^4.5.7"
62
+ },
63
+ "dependencies": {
64
+ "immer": "^10.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@eslint/js": "^9.39.1",
68
+ "@testing-library/dom": "^10.4.1",
69
+ "@testing-library/jest-dom": "^6.9.1",
70
+ "@testing-library/react": "^16.3.0",
71
+ "@testing-library/user-event": "^14.6.1",
72
+ "@types/node": "^24.10.1",
73
+ "@types/react": "^19.2.5",
74
+ "@typescript-eslint/eslint-plugin": "^8.46.4",
75
+ "@typescript-eslint/parser": "^8.46.4",
76
+ "@vitest/ui": "^4.0.9",
77
+ "eslint": "^9.39.1",
78
+ "eslint-config-prettier": "^10.1.8",
79
+ "eslint-plugin-react-hooks": "^7.0.1",
80
+ "happy-dom": "^20.0.10",
81
+ "husky": "^9.1.7",
82
+ "jsdom": "^27.2.0",
83
+ "prettier": "^3.6.2",
84
+ "react": "^19.2.0",
85
+ "react-dom": "^19.2.0",
86
+ "tsup": "^8.5.1",
87
+ "typescript": "^5.3.3",
88
+ "typescript-eslint": "^8.46.4",
89
+ "vitest": "^4.0.9",
90
+ "zustand": "^5.0.8"
91
+ }
92
+ }