62 lines
1.4 KiB
TypeScript
62 lines
1.4 KiB
TypeScript
import React from 'react';
|
||
import { clsx } from 'clsx';
|
||
import { twMerge } from 'tailwind-merge';
|
||
|
||
export type ProgressColor = 'indigo' | 'emerald';
|
||
|
||
export interface ProgressBarProps {
|
||
percent?: number; // 0–100, ignored in indeterminate mode
|
||
color?: ProgressColor;
|
||
indeterminate?: boolean;
|
||
className?: string;
|
||
height?: 'sm' | 'md' | 'lg';
|
||
}
|
||
|
||
const colorMap: Record<ProgressColor, string> = {
|
||
indigo: 'bg-indigo-500',
|
||
emerald: 'bg-emerald-500',
|
||
};
|
||
|
||
const heightMap: Record<string, string> = {
|
||
sm: 'h-1',
|
||
md: 'h-2',
|
||
lg: 'h-3',
|
||
};
|
||
|
||
export const ProgressBar: React.FC<ProgressBarProps> = ({
|
||
percent = 0,
|
||
color = 'indigo',
|
||
indeterminate = false,
|
||
className,
|
||
height = 'md',
|
||
}) => {
|
||
const clamped = Math.max(0, Math.min(100, percent));
|
||
|
||
return (
|
||
<div
|
||
role="progressbar"
|
||
aria-valuenow={indeterminate ? undefined : clamped}
|
||
aria-valuemin={0}
|
||
aria-valuemax={100}
|
||
className={twMerge(
|
||
clsx(
|
||
'w-full rounded-full overflow-hidden bg-gray-800',
|
||
heightMap[height],
|
||
),
|
||
className,
|
||
)}
|
||
>
|
||
<div
|
||
className={clsx(
|
||
'h-full rounded-full transition-all duration-500 ease-out',
|
||
colorMap[color],
|
||
indeterminate && 'animate-[indeterminate_1.5s_ease-in-out_infinite] w-1/3',
|
||
)}
|
||
style={indeterminate ? undefined : { width: `${clamped}%` }}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
ProgressBar.displayName = 'ProgressBar';
|