File size: 3,122 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * AvatarGeneratingLoader — Professional animated loader for avatar generation.
 *
 * Matches the Immagine / Animate processing overlay pattern:
 * purple-themed spinner + status text + optional progress + cancel.
 *
 * Uses the same color palette (purple-400/500/600) and backdrop-blur
 * as the rest of the application for visual consistency.
 */
import React from 'react';
import { Loader2, X } from 'lucide-react';
export function AvatarGeneratingLoader({ label = 'Generating…', hint, progress, onCancel, variant = 'inline', count, aspectClass = 'aspect-[2/3]', }) {
    const isOverlay = variant === 'overlay';
    return (<div className={[
            'flex flex-col items-center justify-center gap-4',
            isOverlay
                ? 'absolute inset-0 z-20 bg-black/50 backdrop-blur-sm rounded-2xl'
                : 'w-full py-8',
        ].join(' ')}>
      {/* Animated spinner — matches Immagine / Animate processing overlay */}
      <div className="relative flex items-center justify-center">
        {/* Outer glow pulse */}
        <div className="absolute w-16 h-16 rounded-full bg-purple-500/10 animate-ping" style={{ animationDuration: '2s' }}/>
        {/* Core spinner icon */}
        <Loader2 size={32} className="text-purple-400 animate-spin"/>
      </div>

      {/* Status text */}
      <div className="flex flex-col items-center gap-1">
        <span className="text-sm font-medium text-white/90">{label}</span>
        {hint && <span className="text-[11px] text-white/40">{hint}</span>}
      </div>

      {/* Progress bar (when provided) */}
      {typeof progress === 'number' && (<div className="w-48 flex flex-col items-center gap-1.5">
          <div className="w-full h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
            <div className="h-full rounded-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-500 ease-out" style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}/>
          </div>
          <span className="text-[10px] text-white/30 tabular-nums">{Math.round(progress)}%</span>
        </div>)}

      {/* Skeleton grid preview */}
      {count && count > 0 && (<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md px-4">
          {Array.from({ length: count }).map((_, i) => (<div key={i} className="rounded-xl overflow-hidden border border-white/[0.06] bg-white/[0.02]">
              <div className={`${aspectClass} bg-gradient-to-br from-purple-900/10 to-transparent flex items-center justify-center`}>
                <div className="w-6 h-6 rounded-full border-2 border-purple-500/20 border-t-purple-400/60 animate-spin" style={{ animationDuration: `${1.2 + i * 0.3}s` }}/>
              </div>
            </div>))}
        </div>)}

      {/* Cancel button */}
      {onCancel && (<button onClick={onCancel} className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-medium border border-red-500/20 bg-red-500/5 text-red-400/80 hover:bg-red-500/15 hover:text-red-400 hover:border-red-500/30 transition-all">
          <X size={12}/> Cancel
        </button>)}
    </div>);
}