Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export type { BreadcrumbProps, BreadcrumbItem } from './common/Breadcrumb';

// Reusable UI primitives
export { default as Button } from './Button';
// UI
export { ProgressBar } from './ui/ProgressBar';
export type { ProgressBarProps, ProgressBarColor, ProgressBarSize } from './ui/ProgressBar';
export { CircularProgress } from './ui/CircularProgress';
export type { CircularProgressProps } from './ui/CircularProgress';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';

export { default as Card, CardHeader, CardBody, CardFooter } from './Card';
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/components/ui/CircularProgress/CircularProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';

export interface CircularProgressProps {
value: number;
size?: number;
strokeWidth?: number;
label?: string;
className?: string;
}

export const CircularProgress: React.FC<CircularProgressProps> = ({
value,
size = 80,
strokeWidth = 6,
label,
className = '',
}) => {
const clamped = Math.min(100, Math.max(0, value));
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (clamped / 100) * circumference;
const center = size / 2;

return (
<div
className={`inline-flex flex-col items-center gap-1.5 ${className}`}
role="progressbar"
aria-valuenow={clamped}
aria-valuemin={0}
aria-valuemax={100}
aria-label={label ?? `Progress: ${clamped}%`}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{/* Track */}
<circle
cx={center}
cy={center}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-accent-blue/20"
/>
{/* Progress arc */}
<circle
cx={center}
cy={center}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
className="text-accent-blue transition-all duration-500 ease-in-out"
transform={`rotate(-90 ${center} ${center})`}
/>
{/* Center percentage label */}
<text
x={center}
y={center}
textAnchor="middle"
dominantBaseline="central"
fontSize={size * 0.2}
fontWeight="600"
fill="white"
>
{clamped}%
</text>
</svg>
{label !== undefined && (
<span className="text-sm text-text-secondary text-center">{label}</span>
)}
</div>
);
};

export default CircularProgress;
2 changes: 2 additions & 0 deletions frontend/src/components/ui/CircularProgress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default, CircularProgress } from './CircularProgress';
export type { CircularProgressProps } from './CircularProgress';
68 changes: 68 additions & 0 deletions frontend/src/components/ui/ProgressBar/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';

export type ProgressBarColor = 'blue' | 'green' | 'yellow' | 'red';
export type ProgressBarSize = 'sm' | 'md' | 'lg';

export interface ProgressBarProps {
value: number;
label?: string;
color?: ProgressBarColor;
size?: ProgressBarSize;
className?: string;
}

const colorStyles: Record<ProgressBarColor, string> = {
blue: 'bg-accent-blue',
green: 'bg-accent-green',
yellow: 'bg-yellow-400',
red: 'bg-accent-red',
};

const trackStyles: Record<ProgressBarColor, string> = {
blue: 'bg-accent-blue/20',
green: 'bg-accent-green/20',
yellow: 'bg-yellow-400/20',
red: 'bg-accent-red/20',
};

const sizeStyles: Record<ProgressBarSize, string> = {
sm: 'h-1',
md: 'h-2',
lg: 'h-3',
};

export const ProgressBar: React.FC<ProgressBarProps> = ({
value,
label,
color = 'blue',
size = 'md',
className = '',
}) => {
const clamped = Math.min(100, Math.max(0, value));

return (
<div className={`w-full ${className}`}>
{label !== undefined && (
<div className="flex justify-between items-center mb-1.5">
<span className="text-sm text-text-secondary">{label}</span>
<span className="text-sm font-semibold text-white">{clamped}%</span>
</div>
)}
<div
className={`w-full rounded-full overflow-hidden ${sizeStyles[size]} ${trackStyles[color]}`}
role="progressbar"
aria-valuenow={clamped}
aria-valuemin={0}
aria-valuemax={100}
aria-label={label ?? `Progress: ${clamped}%`}
>
<div
className={`h-full rounded-full transition-all duration-500 ease-in-out ${colorStyles[color]}`}
style={{ width: `${clamped}%` }}
/>
</div>
</div>
);
};

export default ProgressBar;
2 changes: 2 additions & 0 deletions frontend/src/components/ui/ProgressBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default, ProgressBar } from './ProgressBar';
export type { ProgressBarProps, ProgressBarColor, ProgressBarSize } from './ProgressBar';
Loading