|
1 | | -import { useSyncExternalStore } from "react"; |
2 | | - |
3 | 1 | const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; |
4 | | -const INTERVAL = 80; |
5 | | - |
6 | | -let globalFrameIndex = 0; |
7 | | -let subscriberCount = 0; |
8 | | -let globalTimer: ReturnType<typeof setInterval> | null = null; |
9 | | -const listeners = new Set<() => void>(); |
10 | | - |
11 | | -function subscribe(callback: () => void) { |
12 | | - listeners.add(callback); |
13 | | - subscriberCount++; |
14 | | - if (subscriberCount === 1) { |
15 | | - globalTimer = setInterval(() => { |
16 | | - globalFrameIndex = (globalFrameIndex + 1) % FRAMES.length; |
17 | | - for (const listener of listeners) { |
18 | | - listener(); |
19 | | - } |
20 | | - }, INTERVAL); |
21 | | - } |
22 | | - return () => { |
23 | | - listeners.delete(callback); |
24 | | - subscriberCount--; |
25 | | - if (subscriberCount === 0 && globalTimer) { |
26 | | - clearInterval(globalTimer); |
27 | | - globalTimer = null; |
28 | | - } |
29 | | - }; |
30 | | -} |
31 | | - |
32 | | -function getSnapshot() { |
33 | | - return globalFrameIndex; |
34 | | -} |
| 2 | +const FRAME_INTERVAL_MS = 80; |
35 | 3 |
|
36 | 4 | interface DotsCircleSpinnerProps { |
37 | 5 | size?: number; |
38 | 6 | className?: string; |
39 | 7 | } |
40 | 8 |
|
| 9 | +/** Spins via the `ph-dots-frame` CSS animation (globals.css): all frames are |
| 10 | + * stacked in one grid cell and staggered delays reveal one at a time. Renders |
| 11 | + * once and never updates, so an always-visible spinner costs no JS. */ |
41 | 12 | export function DotsCircleSpinner({ |
42 | 13 | size = 12, |
43 | 14 | className, |
44 | 15 | }: DotsCircleSpinnerProps) { |
45 | | - const frameIndex = useSyncExternalStore(subscribe, getSnapshot); |
46 | | - |
47 | 16 | return ( |
48 | 17 | <span |
49 | | - className={`inline-flex items-center justify-center ${className}`} |
| 18 | + className={`inline-grid place-items-center ${className}`} |
50 | 19 | style={{ |
51 | 20 | width: size, |
52 | 21 | height: size, |
53 | 22 | fontSize: size, |
54 | 23 | lineHeight: 1, |
55 | 24 | }} |
56 | 25 | > |
57 | | - {FRAMES[frameIndex]} |
| 26 | + {FRAMES.map((frame, index) => ( |
| 27 | + <span |
| 28 | + key={frame} |
| 29 | + className="ph-dots-frame" |
| 30 | + style={{ animationDelay: `${index * FRAME_INTERVAL_MS}ms` }} |
| 31 | + > |
| 32 | + {frame} |
| 33 | + </span> |
| 34 | + ))} |
58 | 35 | </span> |
59 | 36 | ); |
60 | 37 | } |
0 commit comments