@rokku-x/react-hook-loading-spinner 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/README.md ADDED
@@ -0,0 +1,639 @@
1
+ # react-hook-loading-spinner
2
+
3
+ A lightweight and flexible React loading state hook library with built-in spinner components, global state management, and zero dependencies (except React and Zustand).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rokku-x/react-hook-loading-spinner
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - 🎯 **Global Loading State** - Centralized loading management across your entire app
14
+ - 🪝 **React Hooks API** - Easy-to-use hook-based interface with automatic cleanup
15
+ - 🔄 **Reference Counting** - Multiple components can start/stop loading independently
16
+ - ⚡ **Async/Await Support** - Built-in async wrapper for promises
17
+ - 🎨 **Customizable Spinners** - Pre-built components or bring your own
18
+ - 📦 **TypeScript Support** - Full type safety out of the box
19
+ - 🎭 **Multiple Animations** - Spin, fade, or no animation
20
+ - 📡 **Event System** - Subscribe to loading state changes
21
+ - ♿ **Accessibility** - Built-in inert attribute and scroll prevention
22
+ - 📱 **Zero Dependencies** - Only requires React and Zustand
23
+
24
+ ## Bundle Size
25
+
26
+ - ESM: 4.23 kB gzipped (12.80 kB raw)
27
+ - CJS: 3.67 kB gzipped (9.31 kB raw)
28
+
29
+ ## Runtime Performance
30
+
31
+ - **startLoading()**: 0.025ms per operation
32
+ - **stopLoading()**: 0.0006ms per operation
33
+ - **start/stop cycle**: 0.023ms per cycle
34
+ - **asyncUseLoading()**: 0.076ms per operation (overhead)
35
+ - **overrideLoading()**: 0.011ms per operation
36
+ - **10 instances start/stop**: 0.269ms per cycle
37
+
38
+ All measurements are extremely fast, with even 1000 operations completing in milliseconds. Measurements based on actual benchmarks in test suite.
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Setup the Loading Renderer
43
+
44
+ First, add the `LoadingRenderer` at the root of your application:
45
+
46
+ ```tsx
47
+ import { LoadingRenderer } from '@rokku-x/react-hook-loading-spinner';
48
+
49
+ function App() {
50
+ return (
51
+ <>
52
+ <YourComponents />
53
+ <LoadingRenderer />
54
+ </>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ### 2. Use the Loading Hook
60
+
61
+ ```tsx
62
+ import { useLoading } from '@rokku-x/react-hook-loading-spinner';
63
+
64
+ function MyComponent() {
65
+ const { startLoading, stopLoading, isLoading } = useLoading();
66
+
67
+ const handleClick = async () => {
68
+ startLoading();
69
+ try {
70
+ await fetchData();
71
+ } finally {
72
+ stopLoading();
73
+ }
74
+ };
75
+
76
+ return (
77
+ <button onClick={handleClick} disabled={isLoading}>
78
+ {isLoading ? 'Loading...' : 'Fetch Data'}
79
+ </button>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## API Reference
85
+
86
+ ### LoadingRenderer
87
+
88
+ The main component that renders the loading spinner overlay. Must be mounted at the root level.
89
+
90
+ #### Props
91
+
92
+ | Prop | Type | Default | Description |
93
+ |------|------|---------|-------------|
94
+ | `loadingComponent` | `React.ComponentType \| React.ReactElement` | `LoadingCircle` | Custom loading component to display |
95
+ | `loadingComponentScale` | `number` | `1` | Scale factor for the loading component |
96
+ | `animationType` | `AnimationType` | `AnimationType.Spin` | Animation type: `'spin'`, `'fadeInOut'`, or `'none'` |
97
+ | `animationDuration` | `number` | `1` (spin) or `2` (fade) | Animation duration in seconds |
98
+ | `wrapperStyle` | `CSSProperties` | `undefined` | Inline styles for the dialog wrapper |
99
+ | `wrapperClassName` | `string` | `undefined` | CSS class for the dialog wrapper |
100
+ | `wrapperId` | `string` | `'loading-wrapper-{random}'` | Unique identifier for the wrapper |
101
+ | `animationWrapperStyle` | `CSSProperties` | `undefined` | Inline styles for the animation wrapper |
102
+ | `animationWrapperClassName` | `string` | `undefined` | CSS class for the animation wrapper |
103
+ | `animationWrapperId` | `string` | `undefined` | ID for the animation wrapper |
104
+
105
+ #### Built-in Loading Components
106
+
107
+ | Component | Description |
108
+ |-----------|-------------|
109
+ | `LoadingCircle` | Spinning circle spinner (default) |
110
+ | `LoadingPleaseWait` | "Please wait..." text |
111
+
112
+ ### useLoading
113
+
114
+ Hook for managing loading state in your components.
115
+
116
+ #### Returns
117
+
118
+ ```typescript
119
+ {
120
+ startLoading: () => void,
121
+ stopLoading: () => void,
122
+ asyncUseLoading: <R>(promise: Promise<R>) => Promise<R>,
123
+ overrideLoading: (state: boolean | null) => void,
124
+ isLoading: boolean,
125
+ isLocalLoading: boolean
126
+ }
127
+ ```
128
+
129
+ | Return Value | Type | Description |
130
+ |---|---|---|
131
+ | `startLoading` | `() => void` | Increment the loading counter (starts loading) |
132
+ | `stopLoading` | `() => void` | Decrement the loading counter (stops loading when reaches 0) |
133
+ | `asyncUseLoading` | `<R>(promise: Promise<R>) => Promise<R>` | Wrap a promise to automatically manage loading state |
134
+ | `overrideLoading` | `(state: boolean \| null) => void` | Override the loading state (null = remove override) |
135
+ | `isLoading` | `boolean` | Global loading state (true if any component is loading) |
136
+ | `isLocalLoading` | `boolean` | Local loading state for this hook instance only |
137
+
138
+ ### Loading Component
139
+
140
+ A component that triggers loading state based on a prop.
141
+
142
+ #### Props
143
+
144
+ | Prop | Type | Default | Description |
145
+ |------|------|---------|-------------|
146
+ | `isLoading` | `boolean` | `false` | Whether to show loading state |
147
+
148
+ #### Example
149
+
150
+ ```tsx
151
+ import { Loading } from 'react-hook-loading-spinner';
152
+
153
+ function MyComponent() {
154
+ const [fetching, setFetching] = useState(false);
155
+
156
+ return (
157
+ <>
158
+ <Loading isLoading={fetching} />
159
+ {/* Your component */}
160
+ </>
161
+ );
162
+ }
163
+ ```
164
+
165
+ ### AnimationType
166
+
167
+ ```typescript
168
+ const AnimationType = {
169
+ Spin: 'spin',
170
+ FadeInOut: 'fadeInOut',
171
+ None: 'none',
172
+ } as const;
173
+ ```
174
+
175
+ ### loadingEventTarget
176
+
177
+ Event emitter for subscribing to loading state changes.
178
+
179
+ #### Events
180
+
181
+ | Event | Payload | Description |
182
+ |-------|---------|-------------|
183
+ | `'change'` | `{ isLoading: boolean, isOverrideState: boolean }` | Emitted whenever loading state changes |
184
+ | `'start'` | `null` | Emitted when loading starts (transitions from false to true) |
185
+ | `'stop'` | `null` | Emitted when loading stops (transitions from true to false) |
186
+
187
+ #### Example
188
+
189
+ ```tsx
190
+ import { loadingEventTarget } from 'react-hook-loading-spinner';
191
+
192
+ useEffect(() => {
193
+ const listener = loadingEventTarget.on('change', ({ isLoading }) => {
194
+ console.log('Loading state changed:', isLoading);
195
+ });
196
+
197
+ return () => listener.removeAllListeners();
198
+ }, []);
199
+ ```
200
+
201
+ ## Examples
202
+
203
+ ### Example 1: Basic Loading
204
+
205
+ ```tsx
206
+ import { useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
207
+
208
+ function App() {
209
+ return (
210
+ <>
211
+ <BasicLoadingExample />
212
+ <LoadingRenderer />
213
+ </>
214
+ );
215
+ }
216
+
217
+ function BasicLoadingExample() {
218
+ const { startLoading, stopLoading } = useLoading();
219
+
220
+ const handleFetch = async () => {
221
+ startLoading();
222
+ try {
223
+ await fetch('https://api.example.com/data');
224
+ } finally {
225
+ stopLoading();
226
+ }
227
+ };
228
+
229
+ return <button onClick={handleFetch}>Fetch Data</button>;
230
+ }
231
+ ```
232
+
233
+ ### Example 2: Async Wrapper
234
+
235
+ The `asyncUseLoading` method automatically manages loading state for promises.
236
+
237
+ ```tsx
238
+ import { useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
239
+
240
+ function AsyncExample() {
241
+ const { asyncUseLoading } = useLoading();
242
+
243
+ const handleFetch = async () => {
244
+ // Loading state is automatically managed
245
+ const data = await asyncUseLoading(
246
+ fetch('https://api.example.com/data').then(res => res.json())
247
+ );
248
+ console.log(data);
249
+ };
250
+
251
+ return <button onClick={handleFetch}>Fetch Data</button>;
252
+ }
253
+
254
+ function App() {
255
+ return (
256
+ <>
257
+ <AsyncExample />
258
+ <LoadingRenderer />
259
+ </>
260
+ );
261
+ }
262
+ ```
263
+
264
+ ### Example 3: Multiple Loading States
265
+
266
+ Multiple components can start loading independently. The loading indicator stays visible until all have stopped.
267
+
268
+ ```tsx
269
+ import { useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
270
+
271
+ function MultipleLoadingExample() {
272
+ const { asyncUseLoading } = useLoading();
273
+
274
+ const fetchUser = () => asyncUseLoading(
275
+ fetch('https://api.example.com/user').then(res => res.json())
276
+ );
277
+
278
+ const fetchPosts = () => asyncUseLoading(
279
+ fetch('https://api.example.com/posts').then(res => res.json())
280
+ );
281
+
282
+ const fetchAll = async () => {
283
+ // Both requests will show loading
284
+ // Loading stops when both complete
285
+ await Promise.all([fetchUser(), fetchPosts()]);
286
+ };
287
+
288
+ return (
289
+ <div>
290
+ <button onClick={fetchUser}>Fetch User</button>
291
+ <button onClick={fetchPosts}>Fetch Posts</button>
292
+ <button onClick={fetchAll}>Fetch All</button>
293
+ </div>
294
+ );
295
+ }
296
+
297
+ function App() {
298
+ return (
299
+ <>
300
+ <MultipleLoadingExample />
301
+ <LoadingRenderer />
302
+ </>
303
+ );
304
+ }
305
+ ```
306
+
307
+ ### Example 4: Local Loading State
308
+
309
+ Track loading state for individual components without affecting global state visibility.
310
+
311
+ ```tsx
312
+ import { useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
313
+
314
+ function LocalLoadingExample() {
315
+ const { startLoading, stopLoading, isLocalLoading } = useLoading();
316
+
317
+ const handleClick = async () => {
318
+ startLoading();
319
+ try {
320
+ await fetch('https://api.example.com/data');
321
+ } finally {
322
+ stopLoading();
323
+ }
324
+ };
325
+
326
+ return (
327
+ <button onClick={handleClick} disabled={isLocalLoading}>
328
+ {isLocalLoading ? 'Loading...' : 'Fetch Data'}
329
+ </button>
330
+ );
331
+ }
332
+
333
+ function App() {
334
+ return (
335
+ <>
336
+ <LocalLoadingExample />
337
+ <LoadingRenderer />
338
+ </>
339
+ );
340
+ }
341
+ ```
342
+
343
+ ### Example 5: Override Loading State
344
+
345
+ Force loading state on or off, bypassing the reference counting.
346
+
347
+ ```tsx
348
+ import { useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
349
+
350
+ function OverrideExample() {
351
+ const { overrideLoading, isLoading } = useLoading();
352
+
353
+ const forceLoading = () => overrideLoading(true);
354
+ const clearOverride = () => overrideLoading(null);
355
+
356
+ return (
357
+ <div>
358
+ <button onClick={forceLoading}>Force Loading</button>
359
+ <button onClick={clearOverride}>Clear Override</button>
360
+ <p>Loading: {isLoading ? 'Yes' : 'No'}</p>
361
+ </div>
362
+ );
363
+ }
364
+
365
+ function App() {
366
+ return (
367
+ <>
368
+ <OverrideExample />
369
+ <LoadingRenderer />
370
+ </>
371
+ );
372
+ }
373
+ ```
374
+
375
+ ### Example 6: Custom Loading Component
376
+
377
+ ```tsx
378
+ import { LoadingRenderer } from 'react-hook-loading-spinner';
379
+
380
+ const CustomSpinner = () => (
381
+ <div style={{
382
+ width: '100px',
383
+ height: '100px',
384
+ border: '10px solid #e0e0e0',
385
+ borderTop: '10px solid #3498db',
386
+ borderRadius: '50%',
387
+ }} />
388
+ );
389
+
390
+ function App() {
391
+ return (
392
+ <>
393
+ <YourComponents />
394
+ <LoadingRenderer
395
+ loadingComponent={CustomSpinner}
396
+ loadingComponentScale={1.5}
397
+ />
398
+ </>
399
+ );
400
+ }
401
+ ```
402
+
403
+ ### Example 7: Fade Animation
404
+
405
+ ```tsx
406
+ import { LoadingRenderer, AnimationType, LoadingPleaseWait } from 'react-hook-loading-spinner';
407
+
408
+ function App() {
409
+ return (
410
+ <>
411
+ <YourComponents />
412
+ <LoadingRenderer
413
+ loadingComponent={LoadingPleaseWait}
414
+ animationType={AnimationType.FadeInOut}
415
+ animationDuration={1.5}
416
+ />
417
+ </>
418
+ );
419
+ }
420
+ ```
421
+
422
+ ### Example 8: Custom Styling
423
+
424
+ ```tsx
425
+ import { LoadingRenderer, LoadingCircle } from 'react-hook-loading-spinner';
426
+
427
+ function App() {
428
+ return (
429
+ <>
430
+ <YourComponents />
431
+ <LoadingRenderer
432
+ loadingComponent={LoadingCircle}
433
+ wrapperStyle={{
434
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
435
+ backdropFilter: 'blur(5px)',
436
+ }}
437
+ animationWrapperStyle={{
438
+ padding: '40px',
439
+ backgroundColor: 'white',
440
+ borderRadius: '16px',
441
+ boxShadow: '0 10px 40px rgba(0,0,0,0.3)',
442
+ }}
443
+ />
444
+ </>
445
+ );
446
+ }
447
+ ```
448
+
449
+ ### Example 9: Loading Component Prop
450
+
451
+ Use the `<Loading>` component to trigger loading based on a boolean prop.
452
+
453
+ ```tsx
454
+ import { Loading, LoadingRenderer } from 'react-hook-loading-spinner';
455
+ import { useState } from 'react';
456
+
457
+ function LoadingComponentExample() {
458
+ const [isFetching, setIsFetching] = useState(false);
459
+
460
+ const handleFetch = async () => {
461
+ setIsFetching(true);
462
+ try {
463
+ await fetch('https://api.example.com/data');
464
+ } finally {
465
+ setIsFetching(false);
466
+ }
467
+ };
468
+
469
+ return (
470
+ <div>
471
+ <Loading isLoading={isFetching} />
472
+ <button onClick={handleFetch}>Fetch Data</button>
473
+ </div>
474
+ );
475
+ }
476
+
477
+ function App() {
478
+ return (
479
+ <>
480
+ <LoadingComponentExample />
481
+ <LoadingRenderer />
482
+ </>
483
+ );
484
+ }
485
+ ```
486
+
487
+ ### Example 10: Event Listener
488
+
489
+ Subscribe to loading state changes using the event system.
490
+
491
+ ```tsx
492
+ import { loadingEventTarget, useLoading, LoadingRenderer } from 'react-hook-loading-spinner';
493
+ import { useEffect, useState } from 'react';
494
+
495
+ function EventListenerExample() {
496
+ const { startLoading, stopLoading } = useLoading();
497
+ const [loadingLog, setLoadingLog] = useState<string[]>([]);
498
+
499
+ useEffect(() => {
500
+ const onChange = loadingEventTarget.on('change', ({ isLoading }) => {
501
+ setLoadingLog(prev => [...prev, `Changed: ${isLoading}`]);
502
+ });
503
+
504
+ const onStart = loadingEventTarget.on('start', () => {
505
+ setLoadingLog(prev => [...prev, 'Started!']);
506
+ });
507
+
508
+ const onStop = loadingEventTarget.on('stop', () => {
509
+ setLoadingLog(prev => [...prev, 'Stopped!']);
510
+ });
511
+
512
+ return () => {
513
+ onChange.removeAllListeners();
514
+ onStart.removeAllListeners();
515
+ onStop.removeAllListeners();
516
+ };
517
+ }, []);
518
+
519
+ return (
520
+ <div>
521
+ <button onClick={startLoading}>Start</button>
522
+ <button onClick={stopLoading}>Stop</button>
523
+ <ul>
524
+ {loadingLog.map((log, i) => <li key={i}>{log}</li>)}
525
+ </ul>
526
+ </div>
527
+ );
528
+ }
529
+
530
+ function App() {
531
+ return (
532
+ <>
533
+ <EventListenerExample />
534
+ <LoadingRenderer />
535
+ </>
536
+ );
537
+ }
538
+ ```
539
+
540
+ ## How It Works
541
+
542
+ ### Reference Counting
543
+
544
+ The library uses a reference counting system to manage loading state:
545
+
546
+ 1. Each call to `startLoading()` increments a counter
547
+ 2. Each call to `stopLoading()` decrements the counter
548
+ 3. Loading is active when the counter > 0
549
+ 4. Multiple components can independently manage loading
550
+
551
+ ### Local Tracking
552
+
553
+ Each `useLoading()` hook instance maintains its own local counter:
554
+
555
+ - `isLocalLoading` reflects only this instance's loading state
556
+ - `startLoading()`/`stopLoading()` only affect the local counter
557
+ - Local counter prevents calling `stopLoading()` more times than `startLoading()`
558
+
559
+ ### Global State
560
+
561
+ The global loading state is managed by Zustand:
562
+
563
+ - `isLoading` reflects whether ANY component is loading
564
+ - Shared across all hook instances
565
+ - Can be overridden with `overrideLoading()`
566
+
567
+ ## Best Practices
568
+
569
+ 1. **Always pair start/stop**: Use try/finally to ensure loading stops even on errors
570
+ ```tsx
571
+ const { startLoading, stopLoading } = useLoading();
572
+
573
+ const fetchData = async () => {
574
+ startLoading();
575
+ try {
576
+ await api.fetchData();
577
+ } finally {
578
+ stopLoading(); // Always called, even on error
579
+ }
580
+ };
581
+ ```
582
+
583
+ 2. **Use async wrapper**: Let the library handle cleanup automatically
584
+ ```tsx
585
+ const { asyncUseLoading } = useLoading();
586
+
587
+ const fetchData = () => asyncUseLoading(api.fetchData());
588
+ ```
589
+
590
+ 3. **Disable buttons during loading**: Prevent duplicate requests
591
+ ```tsx
592
+ const { isLocalLoading } = useLoading();
593
+
594
+ <button disabled={isLocalLoading} onClick={fetchData}>
595
+ Fetch
596
+ </button>
597
+ ```
598
+
599
+ 4. **Clean up event listeners**: Remove listeners in useEffect cleanup
600
+ ```tsx
601
+ useEffect(() => {
602
+ const listener = loadingEventTarget.on('change', handleChange);
603
+ return () => listener.removeAllListeners();
604
+ }, []);
605
+ ```
606
+
607
+ ## TypeScript
608
+
609
+ The library is written in TypeScript and provides full type definitions.
610
+
611
+ ```typescript
612
+ import { useLoading, AnimationType } from 'react-hook-loading-spinner';
613
+ import type { AnimationTypeType } from 'react-hook-loading-spinner';
614
+
615
+ const { startLoading, stopLoading, isLoading } = useLoading();
616
+
617
+ const animation: AnimationTypeType = AnimationType.Spin;
618
+ ```
619
+
620
+ ## Browser Support
621
+
622
+ - Modern browsers (Chrome, Firefox, Safari, Edge)
623
+ - Requires support for:
624
+ - React 18+
625
+ - ES6+ features
626
+ - `<dialog>` element (with polyfill if needed)
627
+ - CSS animations
628
+
629
+ ## License
630
+
631
+ MIT
632
+
633
+ ## Contributing
634
+
635
+ Contributions are welcome! Please feel free to submit a Pull Request.
636
+
637
+ ## Repository
638
+
639
+ [https://github.com/rokku-x/react-hook-loading-spinner](https://github.com/rokku-x/react-hook-loading-spinner)
@@ -0,0 +1,3 @@
1
+ export default function Loading({ isLoading }: {
2
+ isLoading: boolean;
3
+ }): null;
@@ -0,0 +1,21 @@
1
+ import { default as React } from 'react';
2
+ export declare const AnimationType: {
3
+ readonly Spin: "spin";
4
+ readonly FadeInOut: "fadeInOut";
5
+ readonly None: "none";
6
+ };
7
+ export type AnimationType = (typeof AnimationType)[keyof typeof AnimationType];
8
+ export declare const LoadingCircle: React.FC;
9
+ export declare const LoadingPleaseWait: React.FC;
10
+ export default function LoadingRenderer({ loadingComponent, loadingComponentScale, animationType, animationDuration, wrapperStyle, wrapperClassName, wrapperId, animationWrapperStyle, animationWrapperClassName, animationWrapperId }: {
11
+ loadingComponent?: React.ComponentType | React.ReactElement;
12
+ loadingComponentScale?: number;
13
+ animationType?: AnimationType;
14
+ animationDuration?: number;
15
+ wrapperStyle?: React.CSSProperties;
16
+ wrapperClassName?: string;
17
+ wrapperId?: string;
18
+ animationWrapperStyle?: React.CSSProperties;
19
+ animationWrapperClassName?: string;
20
+ animationWrapperId?: string;
21
+ }): React.ReactPortal | null;
@@ -0,0 +1,19 @@
1
+ import { default as EventEmitter } from '../utils/EventEmitter';
2
+ type LoadingEvents = {
3
+ change: {
4
+ isLoading: boolean;
5
+ isOverrideState: boolean;
6
+ } | null;
7
+ start: null;
8
+ stop: null;
9
+ };
10
+ export declare const loadingEventTarget: EventEmitter<LoadingEvents>;
11
+ export default function useLoading(): {
12
+ overrideLoading: (state: boolean | null) => void;
13
+ startLoading: () => void;
14
+ stopLoading: () => void;
15
+ readonly isLocalLoading: boolean;
16
+ asyncUseLoading: <R, _ extends any[]>(asyncFunction: Promise<R>) => Promise<R>;
17
+ readonly isLoading: boolean;
18
+ };
19
+ export {};
@@ -0,0 +1,30 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("react"),h=require("react/jsx-runtime"),N=require("react-dom"),I=n=>{let t;const e=new Set,i=(f,r)=>{const d=typeof f=="function"?f(t):f;if(!Object.is(d,t)){const g=t;t=r??(typeof d!="object"||d===null)?d:Object.assign({},t,d),e.forEach(u=>u(t,g))}},o=()=>t,s={setState:i,getState:o,getInitialState:()=>p,subscribe:f=>(e.add(f),()=>e.delete(f))},p=t=n(i,o,s);return s},D=(n=>n?I(n):I),F=n=>n;function M(n,t=F){const e=m.useSyncExternalStore(n.subscribe,m.useCallback(()=>t(n.getState()),[n,t]),m.useCallback(()=>t(n.getInitialState()),[n,t]));return m.useDebugValue(e),e}const j=n=>{const t=D(n),e=i=>M(t,i);return Object.assign(e,t),e},P=(n=>n?j(n):j),T={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1},C=new Map,x=n=>{const t=C.get(n);return t?Object.fromEntries(Object.entries(t.stores).map(([e,i])=>[e,i.getState()])):{}},$=(n,t,e)=>{if(n===void 0)return{type:"untracked",connection:t.connect(e)};const i=C.get(e.name);if(i)return{type:"tracked",store:n,...i};const o={connection:t.connect(e),stores:{}};return C.set(e.name,o),{type:"tracked",store:n,...o}},J=(n,t)=>{if(t===void 0)return;const e=C.get(n);e&&(delete e.stores[t],Object.keys(e.stores).length===0&&C.delete(n))},U=n=>{var t,e;if(!n)return;const i=n.split(`
2
+ `),o=i.findIndex(v=>v.includes("api.setState"));if(o<0)return;const l=((t=i[o+1])==null?void 0:t.trim())||"";return(e=/.+ (.+) .+/.exec(l))==null?void 0:e[1]},z=(n,t={})=>(e,i,o)=>{const{enabled:l,anonymousActionType:v,store:s,...p}=t;let f;try{f=(l??(T?"production":void 0)!=="production")&&window.__REDUX_DEVTOOLS_EXTENSION__}catch{}if(!f)return n(e,i,o);const{connection:r,...d}=$(s,f,p);let g=!0;o.setState=((c,L,a)=>{const S=e(c,L);if(!g)return S;const O=a===void 0?{type:v||U(new Error().stack)||"anonymous"}:typeof a=="string"?{type:a}:a;return s===void 0?(r?.send(O,i()),S):(r?.send({...O,type:`${s}/${O.type}`},{...x(p.name),[s]:o.getState()}),S)}),o.devtools={cleanup:()=>{r&&typeof r.unsubscribe=="function"&&r.unsubscribe(),J(p.name,s)}};const u=(...c)=>{const L=g;g=!1,e(...c),g=L},E=n(o.setState,i,o);if(d.type==="untracked"?r?.init(E):(d.stores[d.store]=o,r?.init(Object.fromEntries(Object.entries(d.stores).map(([c,L])=>[c,c===d.store?E:L.getState()])))),o.dispatchFromDevtools&&typeof o.dispatch=="function"){let c=!1;const L=o.dispatch;o.dispatch=(...a)=>{(T?"production":void 0)!=="production"&&a[0].type==="__setState"&&!c&&(console.warn('[zustand devtools middleware] "__setState" action type is reserved to set state from the devtools. Avoid using it.'),c=!0),L(...a)}}return r.subscribe(c=>{var L;switch(c.type){case"ACTION":if(typeof c.payload!="string"){console.error("[zustand devtools middleware] Unsupported action format");return}return _(c.payload,a=>{if(a.type==="__setState"){if(s===void 0){u(a.state);return}Object.keys(a.state).length!==1&&console.error(`
3
+ [zustand devtools middleware] Unsupported __setState action format.
4
+ When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),
5
+ and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } }
6
+ `);const S=a.state[s];if(S==null)return;JSON.stringify(o.getState())!==JSON.stringify(S)&&u(S);return}o.dispatchFromDevtools&&typeof o.dispatch=="function"&&o.dispatch(a)});case"DISPATCH":switch(c.payload.type){case"RESET":return u(E),s===void 0?r?.init(o.getState()):r?.init(x(p.name));case"COMMIT":if(s===void 0){r?.init(o.getState());return}return r?.init(x(p.name));case"ROLLBACK":return _(c.state,a=>{if(s===void 0){u(a),r?.init(o.getState());return}u(a[s]),r?.init(x(p.name))});case"JUMP_TO_STATE":case"JUMP_TO_ACTION":return _(c.state,a=>{if(s===void 0){u(a);return}JSON.stringify(o.getState())!==JSON.stringify(a[s])&&u(a[s])});case"IMPORT_STATE":{const{nextLiftedState:a}=c.payload,S=(L=a.computedStates.slice(-1)[0])==null?void 0:L.state;if(!S)return;u(s===void 0?S:S[s]),r?.send(null,a);return}case"PAUSE_RECORDING":return g=!g}return}}),E},W=z,_=(n,t)=>{let e;try{e=JSON.parse(n)}catch(i){console.error("[zustand devtools middleware] Could not parse the received json",i)}e!==void 0&&t(e)};class R extends EventTarget{constructor(){super(...arguments),this.controller=new AbortController}on(t,e){return this.addEventListener(t,i=>e(i.detail),{signal:this.controller.signal}),this}once(t,e){return this.addEventListener(t,i=>e(i.detail),{signal:this.controller.signal,once:!0}),this}emit(t,e){return this.dispatchEvent(new CustomEvent(t,{detail:e}))}removeAllListeners(){this.controller.abort(),this.controller=new AbortController}}const b=new R,V=P()(W((n,t)=>({loadingCount:0,overrideState:null,localCounters:{},isLoading(){return t().overrideState??t().loadingCount>0},actions:{isGlobalLoading(){return t().isLoading()},startLoading:e=>{const i=t().isLoading();n(l=>({loadingCount:l.loadingCount+1})),e&&n(l=>({localCounters:{...l.localCounters,[e]:(l.localCounters[e]??0)+1}}));const o=t().isLoading();o&&!i&&b.emit("start",null),b.emit("change",{isLoading:o,isOverrideState:t().overrideState!==null})},stopLoading:e=>{const i=t().isLoading(),o=e?t().localCounters[e]??0:0;e&&o>0&&n(v=>({loadingCount:Math.max(0,v.loadingCount-1),localCounters:{...v.localCounters,[e]:Math.max(0,v.localCounters[e]-1)}}));const l=t().isLoading();!l&&i&&b.emit("stop",null),b.emit("change",{isLoading:l,isOverrideState:t().overrideState!==null})},overrideLoading:e=>{const i=t().isLoading();n({overrideState:e});const o=t().isLoading();o&&!i?b.emit("start",null):!o&&i&&b.emit("stop",null),b.emit("change",{isLoading:o,isOverrideState:e!==null})},getLocalCounter:e=>t().localCounters[e]??0,isLocalLoading:e=>(t().localCounters[e]??0)>0}})));function w(){const n=m.useId(),{actions:t}=V(l=>l),e=()=>{t.startLoading(n)},i=()=>{t.isLocalLoading(n)&&t.stopLoading(n)},o=async l=>{e();try{return await l}finally{i()}};return{overrideLoading:t.overrideLoading,startLoading:e,stopLoading:i,get isLocalLoading(){return t.isLocalLoading(n)},asyncUseLoading:o,get isLoading(){return t.isGlobalLoading()}}}const y={Spin:"spin",FadeInOut:"fadeInOut",None:"none"},q=({scale:n=1,animationType:t=y.Spin,animationDuration:e,children:i,style:o,className:l,id:v,prefix:s})=>{const p=t,f=t===y.Spin?1:t===y.FadeInOut?2:0,r=e||f,d=t===y.Spin?"linear":t===y.FadeInOut?"ease-in-out":"linear",g=t===y.None?"none":`${s}-${p} ${r}s ${d} infinite`;return h.jsx("div",{style:{animation:g,...n!==1?{zoom:n}:{},...o},className:l,id:v,children:i})},A=n=>h.jsx("div",{id:"loading-circle",style:{width:"90px",height:"90px",border:"15px solid #f3f3f3",borderTop:"15px solid #009b4bff",borderRadius:"50%",boxSizing:"border-box"},...n}),k=n=>h.jsx("div",{style:{padding:"20px",fontSize:"25px",color:"#333",fontFamily:"system-ui, sans-serif"},...n,children:"Please wait..."});function B({loadingComponent:n,loadingComponentScale:t=1,animationType:e=y.Spin,animationDuration:i,wrapperStyle:o,wrapperClassName:l,wrapperId:v,animationWrapperStyle:s,animationWrapperClassName:p,animationWrapperId:f}){n=n||(e===y.Spin?A:k);const r=m.useRef(Math.random().toString(36).substring(2,6).replace(/[0-9]/g,"")),{isLoading:d}=w(),g=m.useRef(null);m.useEffect(()=>{d?(g.current?.showModal(),document.body.setAttribute("inert","")):(g.current?.close(),document.body.removeAttribute("inert"))},[d]);const u=v||"loading-wrapper-"+r.current;return d?N.createPortal(h.jsxs(h.Fragment,{children:[h.jsx("style",{children:`
7
+ dialog#${u}[open] {
8
+ display: flex !important;
9
+ justify-content: center;
10
+ align-items: center;
11
+ width: 100vw;
12
+ height: 100vh;
13
+ max-width: 100%;
14
+ max-height: 100%;
15
+ }
16
+ @keyframes ${r.current}-spin {
17
+ 0% { transform: rotate(0deg); }
18
+ 100% { transform: rotate(360deg); }
19
+ }
20
+ @keyframes ${r.current}-fadeInOut {
21
+ 0%, 100% { opacity: 0.2; }
22
+ 50% { opacity: 1; }
23
+ }
24
+ body:has(dialog#${u}[open]) {
25
+ overflow: hidden;
26
+ }
27
+ body {
28
+ scrollbar-gutter: stable;
29
+ }
30
+ `}),h.jsx("dialog",{ref:g,style:{border:"none",padding:0,backgroundColor:"rgba(0, 0, 0, 0.5)",backdropFilter:"blur(2px)",...o},className:l,id:u,children:h.jsx(q,{scale:t,animationType:e,animationDuration:i,style:s,className:p,id:f,prefix:r.current,children:m.isValidElement(n)?n:m.createElement(n)})})]}),document.body):null}function G({isLoading:n=!1}){const{startLoading:t,stopLoading:e}=w();return m.useEffect(()=>(n?t():e(),()=>{n&&e()}),[n]),null}exports.AnimationType=y;exports.EventEmitter=R;exports.Loading=G;exports.LoadingCircle=A;exports.LoadingPleaseWait=k;exports.LoadingRenderer=B;exports.loadingEventTarget=b;exports.useLoading=w;
@@ -0,0 +1,7 @@
1
+ export { default as useLoading } from './hooks/useLoading';
2
+ export { loadingEventTarget } from './hooks/useLoading';
3
+ export { default as LoadingRenderer } from './components/LoadingRenderer';
4
+ export { LoadingCircle, LoadingPleaseWait, AnimationType } from './components/LoadingRenderer';
5
+ export type { AnimationType as AnimationTypeType } from './components/LoadingRenderer';
6
+ export { default as Loading } from './components/Loading';
7
+ export { default as EventEmitter } from './utils/EventEmitter';
@@ -0,0 +1,387 @@
1
+ "use client";
2
+ import y, { useId as N, useRef as x, useEffect as k } from "react";
3
+ import { jsxs as j, Fragment as D, jsx as L } from "react/jsx-runtime";
4
+ import { createPortal as F } from "react-dom";
5
+ const I = (n) => {
6
+ let t;
7
+ const e = /* @__PURE__ */ new Set(), r = (f, i) => {
8
+ const d = typeof f == "function" ? f(t) : f;
9
+ if (!Object.is(d, t)) {
10
+ const g = t;
11
+ t = i ?? (typeof d != "object" || d === null) ? d : Object.assign({}, t, d), e.forEach((u) => u(t, g));
12
+ }
13
+ }, o = () => t, s = { setState: r, getState: o, getInitialState: () => p, subscribe: (f) => (e.add(f), () => e.delete(f)) }, p = t = n(r, o, s);
14
+ return s;
15
+ }, M = ((n) => n ? I(n) : I), $ = (n) => n;
16
+ function P(n, t = $) {
17
+ const e = y.useSyncExternalStore(
18
+ n.subscribe,
19
+ y.useCallback(() => t(n.getState()), [n, t]),
20
+ y.useCallback(() => t(n.getInitialState()), [n, t])
21
+ );
22
+ return y.useDebugValue(e), e;
23
+ }
24
+ const T = (n) => {
25
+ const t = M(n), e = (r) => P(t, r);
26
+ return Object.assign(e, t), e;
27
+ }, J = ((n) => n ? T(n) : T), R = { BASE_URL: "/", DEV: !1, MODE: "production", PROD: !0, SSR: !1 }, C = /* @__PURE__ */ new Map(), w = (n) => {
28
+ const t = C.get(n);
29
+ return t ? Object.fromEntries(
30
+ Object.entries(t.stores).map(([e, r]) => [e, r.getState()])
31
+ ) : {};
32
+ }, U = (n, t, e) => {
33
+ if (n === void 0)
34
+ return {
35
+ type: "untracked",
36
+ connection: t.connect(e)
37
+ };
38
+ const r = C.get(e.name);
39
+ if (r)
40
+ return { type: "tracked", store: n, ...r };
41
+ const o = {
42
+ connection: t.connect(e),
43
+ stores: {}
44
+ };
45
+ return C.set(e.name, o), { type: "tracked", store: n, ...o };
46
+ }, z = (n, t) => {
47
+ if (t === void 0) return;
48
+ const e = C.get(n);
49
+ e && (delete e.stores[t], Object.keys(e.stores).length === 0 && C.delete(n));
50
+ }, V = (n) => {
51
+ var t, e;
52
+ if (!n) return;
53
+ const r = n.split(`
54
+ `), o = r.findIndex(
55
+ (v) => v.includes("api.setState")
56
+ );
57
+ if (o < 0) return;
58
+ const l = ((t = r[o + 1]) == null ? void 0 : t.trim()) || "";
59
+ return (e = /.+ (.+) .+/.exec(l)) == null ? void 0 : e[1];
60
+ }, W = (n, t = {}) => (e, r, o) => {
61
+ const { enabled: l, anonymousActionType: v, store: s, ...p } = t;
62
+ let f;
63
+ try {
64
+ f = (l ?? (R ? "production" : void 0) !== "production") && window.__REDUX_DEVTOOLS_EXTENSION__;
65
+ } catch {
66
+ }
67
+ if (!f)
68
+ return n(e, r, o);
69
+ const { connection: i, ...d } = U(s, f, p);
70
+ let g = !0;
71
+ o.setState = ((c, m, a) => {
72
+ const S = e(c, m);
73
+ if (!g) return S;
74
+ const O = a === void 0 ? {
75
+ type: v || V(new Error().stack) || "anonymous"
76
+ } : typeof a == "string" ? { type: a } : a;
77
+ return s === void 0 ? (i?.send(O, r()), S) : (i?.send(
78
+ {
79
+ ...O,
80
+ type: `${s}/${O.type}`
81
+ },
82
+ {
83
+ ...w(p.name),
84
+ [s]: o.getState()
85
+ }
86
+ ), S);
87
+ }), o.devtools = {
88
+ cleanup: () => {
89
+ i && typeof i.unsubscribe == "function" && i.unsubscribe(), z(p.name, s);
90
+ }
91
+ };
92
+ const u = (...c) => {
93
+ const m = g;
94
+ g = !1, e(...c), g = m;
95
+ }, _ = n(o.setState, r, o);
96
+ if (d.type === "untracked" ? i?.init(_) : (d.stores[d.store] = o, i?.init(
97
+ Object.fromEntries(
98
+ Object.entries(d.stores).map(([c, m]) => [
99
+ c,
100
+ c === d.store ? _ : m.getState()
101
+ ])
102
+ )
103
+ )), o.dispatchFromDevtools && typeof o.dispatch == "function") {
104
+ let c = !1;
105
+ const m = o.dispatch;
106
+ o.dispatch = (...a) => {
107
+ (R ? "production" : void 0) !== "production" && a[0].type === "__setState" && !c && (console.warn(
108
+ '[zustand devtools middleware] "__setState" action type is reserved to set state from the devtools. Avoid using it.'
109
+ ), c = !0), m(...a);
110
+ };
111
+ }
112
+ return i.subscribe((c) => {
113
+ var m;
114
+ switch (c.type) {
115
+ case "ACTION":
116
+ if (typeof c.payload != "string") {
117
+ console.error(
118
+ "[zustand devtools middleware] Unsupported action format"
119
+ );
120
+ return;
121
+ }
122
+ return E(
123
+ c.payload,
124
+ (a) => {
125
+ if (a.type === "__setState") {
126
+ if (s === void 0) {
127
+ u(a.state);
128
+ return;
129
+ }
130
+ Object.keys(a.state).length !== 1 && console.error(
131
+ `
132
+ [zustand devtools middleware] Unsupported __setState action format.
133
+ When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),
134
+ and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } }
135
+ `
136
+ );
137
+ const S = a.state[s];
138
+ if (S == null)
139
+ return;
140
+ JSON.stringify(o.getState()) !== JSON.stringify(S) && u(S);
141
+ return;
142
+ }
143
+ o.dispatchFromDevtools && typeof o.dispatch == "function" && o.dispatch(a);
144
+ }
145
+ );
146
+ case "DISPATCH":
147
+ switch (c.payload.type) {
148
+ case "RESET":
149
+ return u(_), s === void 0 ? i?.init(o.getState()) : i?.init(w(p.name));
150
+ case "COMMIT":
151
+ if (s === void 0) {
152
+ i?.init(o.getState());
153
+ return;
154
+ }
155
+ return i?.init(w(p.name));
156
+ case "ROLLBACK":
157
+ return E(c.state, (a) => {
158
+ if (s === void 0) {
159
+ u(a), i?.init(o.getState());
160
+ return;
161
+ }
162
+ u(a[s]), i?.init(w(p.name));
163
+ });
164
+ case "JUMP_TO_STATE":
165
+ case "JUMP_TO_ACTION":
166
+ return E(c.state, (a) => {
167
+ if (s === void 0) {
168
+ u(a);
169
+ return;
170
+ }
171
+ JSON.stringify(o.getState()) !== JSON.stringify(a[s]) && u(a[s]);
172
+ });
173
+ case "IMPORT_STATE": {
174
+ const { nextLiftedState: a } = c.payload, S = (m = a.computedStates.slice(-1)[0]) == null ? void 0 : m.state;
175
+ if (!S) return;
176
+ u(s === void 0 ? S : S[s]), i?.send(
177
+ null,
178
+ // FIXME no-any
179
+ a
180
+ );
181
+ return;
182
+ }
183
+ case "PAUSE_RECORDING":
184
+ return g = !g;
185
+ }
186
+ return;
187
+ }
188
+ }), _;
189
+ }, B = W, E = (n, t) => {
190
+ let e;
191
+ try {
192
+ e = JSON.parse(n);
193
+ } catch (r) {
194
+ console.error(
195
+ "[zustand devtools middleware] Could not parse the received json",
196
+ r
197
+ );
198
+ }
199
+ e !== void 0 && t(e);
200
+ };
201
+ class G extends EventTarget {
202
+ constructor() {
203
+ super(...arguments), this.controller = new AbortController();
204
+ }
205
+ on(t, e) {
206
+ return this.addEventListener(
207
+ t,
208
+ (r) => e(r.detail),
209
+ { signal: this.controller.signal }
210
+ ), this;
211
+ }
212
+ once(t, e) {
213
+ return this.addEventListener(
214
+ t,
215
+ (r) => e(r.detail),
216
+ {
217
+ signal: this.controller.signal,
218
+ once: !0
219
+ }
220
+ ), this;
221
+ }
222
+ emit(t, e) {
223
+ return this.dispatchEvent(new CustomEvent(t, { detail: e }));
224
+ }
225
+ removeAllListeners() {
226
+ this.controller.abort(), this.controller = new AbortController();
227
+ }
228
+ }
229
+ const b = new G(), X = J()(B((n, t) => ({
230
+ loadingCount: 0,
231
+ overrideState: null,
232
+ localCounters: {},
233
+ isLoading() {
234
+ return t().overrideState ?? t().loadingCount > 0;
235
+ },
236
+ actions: {
237
+ isGlobalLoading() {
238
+ return t().isLoading();
239
+ },
240
+ startLoading: (e) => {
241
+ const r = t().isLoading();
242
+ n((l) => ({ loadingCount: l.loadingCount + 1 })), e && n((l) => ({
243
+ localCounters: {
244
+ ...l.localCounters,
245
+ [e]: (l.localCounters[e] ?? 0) + 1
246
+ }
247
+ }));
248
+ const o = t().isLoading();
249
+ o && !r && b.emit("start", null), b.emit("change", { isLoading: o, isOverrideState: t().overrideState !== null });
250
+ },
251
+ stopLoading: (e) => {
252
+ const r = t().isLoading(), o = e ? t().localCounters[e] ?? 0 : 0;
253
+ e && o > 0 && n((v) => ({
254
+ loadingCount: Math.max(0, v.loadingCount - 1),
255
+ localCounters: {
256
+ ...v.localCounters,
257
+ [e]: Math.max(0, v.localCounters[e] - 1)
258
+ }
259
+ }));
260
+ const l = t().isLoading();
261
+ !l && r && b.emit("stop", null), b.emit("change", { isLoading: l, isOverrideState: t().overrideState !== null });
262
+ },
263
+ overrideLoading: (e) => {
264
+ const r = t().isLoading();
265
+ n({ overrideState: e });
266
+ const o = t().isLoading();
267
+ o && !r ? b.emit("start", null) : !o && r && b.emit("stop", null), b.emit("change", { isLoading: o, isOverrideState: e !== null });
268
+ },
269
+ getLocalCounter: (e) => t().localCounters[e] ?? 0,
270
+ isLocalLoading: (e) => (t().localCounters[e] ?? 0) > 0
271
+ }
272
+ })));
273
+ function A() {
274
+ const n = N(), { actions: t } = X((l) => l), e = () => {
275
+ t.startLoading(n);
276
+ }, r = () => {
277
+ t.isLocalLoading(n) && t.stopLoading(n);
278
+ }, o = async (l) => {
279
+ e();
280
+ try {
281
+ return await l;
282
+ } finally {
283
+ r();
284
+ }
285
+ };
286
+ return {
287
+ overrideLoading: t.overrideLoading,
288
+ startLoading: e,
289
+ stopLoading: r,
290
+ get isLocalLoading() {
291
+ return t.isLocalLoading(n);
292
+ },
293
+ asyncUseLoading: o,
294
+ get isLoading() {
295
+ return t.isGlobalLoading();
296
+ }
297
+ };
298
+ }
299
+ const h = {
300
+ Spin: "spin",
301
+ FadeInOut: "fadeInOut",
302
+ None: "none"
303
+ }, H = ({ scale: n = 1, animationType: t = h.Spin, animationDuration: e, children: r, style: o, className: l, id: v, prefix: s }) => {
304
+ const p = t, f = t === h.Spin ? 1 : t === h.FadeInOut ? 2 : 0, i = e || f, d = t === h.Spin ? "linear" : t === h.FadeInOut ? "ease-in-out" : "linear", g = t === h.None ? "none" : `${s}-${p} ${i}s ${d} infinite`;
305
+ return /* @__PURE__ */ L("div", { style: { animation: g, ...n !== 1 ? { zoom: n } : {}, ...o }, className: l, id: v, children: r });
306
+ }, K = (n) => /* @__PURE__ */ L("div", { id: "loading-circle", style: { width: "90px", height: "90px", border: "15px solid #f3f3f3", borderTop: "15px solid #009b4bff", borderRadius: "50%", boxSizing: "border-box" }, ...n }), q = (n) => /* @__PURE__ */ L("div", { style: { padding: "20px", fontSize: "25px", color: "#333", fontFamily: "system-ui, sans-serif" }, ...n, children: "Please wait..." });
307
+ function tt({
308
+ loadingComponent: n,
309
+ loadingComponentScale: t = 1,
310
+ animationType: e = h.Spin,
311
+ animationDuration: r,
312
+ wrapperStyle: o,
313
+ wrapperClassName: l,
314
+ wrapperId: v,
315
+ animationWrapperStyle: s,
316
+ animationWrapperClassName: p,
317
+ animationWrapperId: f
318
+ }) {
319
+ n = n || (e === h.Spin ? K : q);
320
+ const i = x(Math.random().toString(36).substring(2, 6).replace(/[0-9]/g, "")), { isLoading: d } = A(), g = x(null);
321
+ k(() => {
322
+ d ? (g.current?.showModal(), document.body.setAttribute("inert", "")) : (g.current?.close(), document.body.removeAttribute("inert"));
323
+ }, [d]);
324
+ const u = v || "loading-wrapper-" + i.current;
325
+ return d ? F(
326
+ /* @__PURE__ */ j(D, { children: [
327
+ /* @__PURE__ */ L("style", { children: `
328
+ dialog#${u}[open] {
329
+ display: flex !important;
330
+ justify-content: center;
331
+ align-items: center;
332
+ width: 100vw;
333
+ height: 100vh;
334
+ max-width: 100%;
335
+ max-height: 100%;
336
+ }
337
+ @keyframes ${i.current}-spin {
338
+ 0% { transform: rotate(0deg); }
339
+ 100% { transform: rotate(360deg); }
340
+ }
341
+ @keyframes ${i.current}-fadeInOut {
342
+ 0%, 100% { opacity: 0.2; }
343
+ 50% { opacity: 1; }
344
+ }
345
+ body:has(dialog#${u}[open]) {
346
+ overflow: hidden;
347
+ }
348
+ body {
349
+ scrollbar-gutter: stable;
350
+ }
351
+ ` }),
352
+ /* @__PURE__ */ L(
353
+ "dialog",
354
+ {
355
+ ref: g,
356
+ style: {
357
+ border: "none",
358
+ padding: 0,
359
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
360
+ backdropFilter: "blur(2px)",
361
+ ...o
362
+ },
363
+ className: l,
364
+ id: u,
365
+ children: /* @__PURE__ */ L(H, { scale: t, animationType: e, animationDuration: r, style: s, className: p, id: f, prefix: i.current, children: y.isValidElement(n) ? n : y.createElement(n) })
366
+ }
367
+ )
368
+ ] }),
369
+ document.body
370
+ ) : null;
371
+ }
372
+ function et({ isLoading: n = !1 }) {
373
+ const { startLoading: t, stopLoading: e } = A();
374
+ return k(() => (n ? t() : e(), () => {
375
+ n && e();
376
+ }), [n]), null;
377
+ }
378
+ export {
379
+ h as AnimationType,
380
+ G as EventEmitter,
381
+ et as Loading,
382
+ K as LoadingCircle,
383
+ q as LoadingPleaseWait,
384
+ tt as LoadingRenderer,
385
+ b as loadingEventTarget,
386
+ A as useLoading
387
+ };
@@ -0,0 +1,9 @@
1
+ type EventMap = Record<string, any>;
2
+ export default class EventEmitter<T extends EventMap> extends EventTarget {
3
+ private controller;
4
+ on<K extends keyof T>(type: K, callback: (detail: T[K]) => void): this;
5
+ once<K extends keyof T>(type: K, callback: (detail: T[K]) => void): this;
6
+ emit<K extends keyof T>(type: K, detail: T[K]): boolean;
7
+ removeAllListeners(): void;
8
+ }
9
+ export {};
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@rokku-x/react-hook-loading-spinner",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight and flexible React loading state hook library with built-in spinner components, global state management, and zero dependencies (except React and Zustand).",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "vite build",
14
+ "prepare": "npm run build",
15
+ "dev": "vite",
16
+ "preview": "vite preview",
17
+ "test": "vitest"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/rokku-x/react-hook-loading-spinner.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/rokku-x/react-hook-loading-spinner/issues"
25
+ },
26
+ "homepage": "https://github.com/rokku-x/react-hook-loading-spinner#readme",
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.0.0",
33
+ "react-dom": "^18.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/jest-dom": "^6.0.0",
37
+ "@testing-library/react": "^14.0.0",
38
+ "@testing-library/user-event": "^14.5.2",
39
+ "@types/jest": "^30.0.0",
40
+ "@types/react": "^18.0.0",
41
+ "@types/react-dom": "^18.0.0",
42
+ "@vitejs/plugin-react": "^5.0.4",
43
+ "jsdom": "^27.4.0",
44
+ "typescript": "^5.0.0",
45
+ "vite": "^7.1.9",
46
+ "vite-plugin-dts": "^4.5.4",
47
+ "vitest": "^1.0.0"
48
+ },
49
+ "sideEffects": false,
50
+ "exports": {
51
+ ".": {
52
+ "types": "./dist/index.d.ts",
53
+ "import": "./dist/index.esm.js",
54
+ "require": "./dist/index.cjs.js"
55
+ },
56
+ "./package.json": "./package.json"
57
+ },
58
+ "typesVersions": {
59
+ "*": {
60
+ "*": [
61
+ "dist/*"
62
+ ]
63
+ }
64
+ },
65
+ "keywords": [
66
+ "react",
67
+ "loading",
68
+ "spinner",
69
+ "hook",
70
+ "state",
71
+ "component",
72
+ "library",
73
+ "zustand",
74
+ "typescript"
75
+ ],
76
+ "author": "rokku-x",
77
+ "license": "MIT",
78
+ "dependencies": {
79
+ "zustand": "^5.0.10"
80
+ }
81
+ }