veza/apps/web/desy/components/ui/progress.tsx
2026-01-22 17:23:11 +01:00

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 }