188 lines
4.8 KiB
TypeScript
188 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const progressVariants = cva(
|
|
'relative w-full overflow-hidden rounded-full bg-muted',
|
|
{
|
|
variants: {
|
|
size: {
|
|
sm: 'h-1',
|
|
default: 'h-2',
|
|
lg: 'h-3',
|
|
xl: 'h-4',
|
|
},
|
|
variant: {
|
|
default: '',
|
|
gradient: '',
|
|
success: '',
|
|
warning: '',
|
|
danger: '',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'default',
|
|
variant: 'default',
|
|
},
|
|
},
|
|
)
|
|
|
|
const indicatorVariants = cva(
|
|
'h-full w-full flex-1 transition-all duration-500 ease-out',
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: 'bg-primary',
|
|
gradient: 'bg-gradient-to-r from-primary to-secondary',
|
|
success: 'bg-success',
|
|
warning: 'bg-warning',
|
|
danger: 'bg-destructive',
|
|
},
|
|
animated: {
|
|
true: '',
|
|
false: '',
|
|
},
|
|
},
|
|
compoundVariants: [
|
|
{
|
|
animated: true,
|
|
className: 'relative after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/20 after:to-transparent after:animate-[shimmer_2s_ease-in-out_infinite]',
|
|
},
|
|
],
|
|
defaultVariants: {
|
|
variant: 'default',
|
|
animated: false,
|
|
},
|
|
},
|
|
)
|
|
|
|
interface ProgressProps
|
|
extends React.ComponentProps<typeof ProgressPrimitive.Root>,
|
|
VariantProps<typeof progressVariants> {
|
|
showValue?: boolean
|
|
animated?: boolean
|
|
label?: string
|
|
}
|
|
|
|
function Progress({
|
|
className,
|
|
value = 0,
|
|
size,
|
|
variant,
|
|
showValue = false,
|
|
animated = false,
|
|
label,
|
|
...props
|
|
}: ProgressProps) {
|
|
return (
|
|
<div className="w-full">
|
|
{(label || showValue) && (
|
|
<div className="mb-2 flex items-center justify-between text-sm">
|
|
{label && <span className="font-medium text-foreground">{label}</span>}
|
|
{showValue && (
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{Math.round(value || 0)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
<ProgressPrimitive.Root
|
|
data-slot="progress"
|
|
className={cn(progressVariants({ size, variant }), className)}
|
|
{...props}
|
|
>
|
|
<ProgressPrimitive.Indicator
|
|
data-slot="progress-indicator"
|
|
className={cn(indicatorVariants({ variant, animated }))}
|
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
/>
|
|
</ProgressPrimitive.Root>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Circular Progress
|
|
interface CircularProgressProps {
|
|
value?: number
|
|
size?: number
|
|
strokeWidth?: number
|
|
variant?: 'default' | 'gradient'
|
|
showValue?: boolean
|
|
className?: string
|
|
}
|
|
|
|
function CircularProgress({
|
|
value = 0,
|
|
size = 48,
|
|
strokeWidth = 4,
|
|
variant = 'default',
|
|
showValue = false,
|
|
className,
|
|
}: CircularProgressProps) {
|
|
const radius = (size - strokeWidth) / 2
|
|
const circumference = radius * 2 * Math.PI
|
|
const offset = circumference - (value / 100) * circumference
|
|
|
|
return (
|
|
<div className={cn('relative inline-flex items-center justify-center', className)}>
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
className="rotate-[-90deg]"
|
|
>
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={strokeWidth}
|
|
className="text-muted"
|
|
/>
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
fill="none"
|
|
stroke={variant === 'gradient' ? 'url(#gradient)' : 'currentColor'}
|
|
strokeWidth={strokeWidth}
|
|
strokeLinecap="round"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={offset}
|
|
className={cn(
|
|
'transition-all duration-500 ease-out',
|
|
variant === 'default' && 'text-primary'
|
|
)}
|
|
/>
|
|
{variant === 'gradient' && (
|
|
<defs>
|
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" stopColor="oklch(0.75 0.18 195)" />
|
|
<stop offset="100%" stopColor="oklch(0.65 0.25 330)" />
|
|
</linearGradient>
|
|
</defs>
|
|
)}
|
|
</svg>
|
|
{showValue && (
|
|
<span className="absolute font-mono text-xs font-semibold">
|
|
{Math.round(value)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Indeterminate Loader Bar
|
|
function LoaderBar({ className }: { className?: string }) {
|
|
return (
|
|
<div className={cn('relative h-1 w-full overflow-hidden rounded-full bg-muted', className)}>
|
|
<div className="absolute h-full w-1/3 animate-[indeterminate_1.5s_ease-in-out_infinite] rounded-full bg-gradient-to-r from-primary to-secondary" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { Progress, CircularProgress, LoaderBar, progressVariants }
|