'use client';
import { useEffect, useState } from 'react';
import { Download, Pause, Play, RotateCcw, Upload, Zap } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Progress } from '@kit/ui/progress';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Slider } from '@kit/ui/slider';
import { Switch } from '@kit/ui/switch';
import {
generateImportStatement,
generatePropsString,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface ProgressControlsProps {
value: number;
max: number;
className: string;
size: 'sm' | 'default' | 'lg';
variant: 'default' | 'success' | 'warning' | 'error';
animated: boolean;
showLabel: boolean;
indeterminate: boolean;
onValueChange: (value: number) => void;
onMaxChange: (max: number) => void;
onClassNameChange: (className: string) => void;
onSizeChange: (size: 'sm' | 'default' | 'lg') => void;
onVariantChange: (
variant: 'default' | 'success' | 'warning' | 'error',
) => void;
onAnimatedChange: (animated: boolean) => void;
onShowLabelChange: (showLabel: boolean) => void;
onIndeterminateChange: (indeterminate: boolean) => void;
}
const progressControls = [
{
name: 'value',
type: 'range',
min: 0,
max: 100,
step: 1,
description: 'Current progress value',
},
{
name: 'max',
type: 'range',
min: 50,
max: 200,
step: 10,
description: 'Maximum progress value',
},
{
name: 'size',
type: 'select',
options: ['sm', 'default', 'lg'],
description: 'Progress bar size',
},
{
name: 'variant',
type: 'select',
options: ['default', 'success', 'warning', 'error'],
description: 'Progress bar color variant',
},
{
name: 'animated',
type: 'boolean',
description: 'Enable animated progress bar',
},
{
name: 'showLabel',
type: 'boolean',
description: 'Show percentage label',
},
{
name: 'indeterminate',
type: 'boolean',
description: 'Indeterminate/loading state',
},
{
name: 'className',
type: 'text',
description: 'Additional CSS classes',
},
];
const sizeClasses = {
sm: 'h-1',
default: 'h-2',
lg: 'h-4',
};
const variantClasses = {
default: '',
success: '[&>*]:bg-green-500',
warning: '[&>*]:bg-yellow-500',
error: '[&>*]:bg-red-500',
};
function ProgressPlayground({
value,
max,
className,
size,
variant,
animated,
showLabel,
indeterminate,
}: ProgressControlsProps) {
const [animatedValue, setAnimatedValue] = useState(0);
useEffect(() => {
if (indeterminate) return;
const timer = setTimeout(() => setAnimatedValue(value), 100);
return () => clearTimeout(timer);
}, [value, indeterminate]);
const displayValue = indeterminate
? undefined
: animated
? animatedValue
: value;
const percentage = indeterminate
? 0
: Math.round((displayValue! / max) * 100);
return (
{showLabel && !indeterminate && (
Progress
{percentage}%
)}
);
}
const examples = [
{
title: 'File Upload Progress',
description: 'Track file upload with success state and percentage display',
component: () => {
const [progress, setProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const startUpload = () => {
setIsUploading(true);
setProgress(0);
const interval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
setIsUploading(false);
clearInterval(interval);
return 100;
}
return prev + Math.random() * 15;
});
}, 300);
};
const reset = () => {
setProgress(0);
setIsUploading(false);
};
return (
Upload Documents
Progress
{Math.round(progress)}%
);
},
},
{
title: 'Download Manager',
description: 'Multiple progress bars with different states and sizes',
component: () => {
const downloads = [
{
id: 1,
name: 'Project Files.zip',
progress: 100,
size: 'default' as const,
variant: 'success' as const,
},
{
id: 2,
name: 'Software Update.dmg',
progress: 67,
size: 'default' as const,
variant: 'default' as const,
},
{
id: 3,
name: 'Documentation.pdf',
progress: 23,
size: 'default' as const,
variant: 'warning' as const,
},
{
id: 4,
name: 'Failed Download.exe',
progress: 45,
size: 'default' as const,
variant: 'error' as const,
},
];
return (
Download Manager
{downloads.map((download) => (
{download.name}
{download.progress}%
{download.progress === 100
? 'Completed'
: download.variant === 'error'
? 'Failed'
: 'Downloading...'}
{download.progress}/100
))}
);
},
},
{
title: 'Skill Levels',
description:
'Progress bars showing different skill levels with custom styling',
component: () => {
const skills = [
{ name: 'React', level: 90, color: '[&>*]:bg-blue-500' },
{ name: 'TypeScript', level: 85, color: '[&>*]:bg-blue-600' },
{ name: 'Node.js', level: 75, color: '[&>*]:bg-green-600' },
{ name: 'Python', level: 60, color: '[&>*]:bg-yellow-500' },
{ name: 'Go', level: 40, color: '[&>*]:bg-cyan-500' },
];
return (
Skills Overview
{skills.map((skill) => (
{skill.name}
{skill.level}%
))}
);
},
},
{
title: 'Loading States',
description: 'Indeterminate progress for unknown durations',
component: () => {
const [states, setStates] = useState({
processing: true,
analyzing: true,
syncing: true,
});
const toggleState = (key: keyof typeof states) => {
setStates((prev) => ({ ...prev, [key]: !prev[key] }));
};
return (
Loading Operations
Processing data...
Analyzing files...
Syncing changes...
);
},
},
];
const apiReference = {
title: 'Progress API Reference',
description: 'Complete API documentation for the Progress component.',
props: [
{
name: 'value',
type: 'number | undefined',
default: 'undefined',
description:
'The current progress value. Use undefined for indeterminate state.',
},
{
name: 'max',
type: 'number',
default: '100',
description: 'The maximum progress value.',
},
{
name: 'className',
type: 'string',
description: 'Additional CSS classes to apply to the progress container.',
},
{
name: '...props',
type: 'React.ComponentProps',
description:
'All props from Radix UI Progress.Root component including aria-label, aria-labelledby, aria-describedby, etc.',
},
],
examples: [
{
title: 'Basic Usage',
code: `import { Progress } from '@kit/ui/progress';
`,
},
{
title: 'Custom Maximum',
code: ``,
},
{
title: 'Indeterminate State',
code: ``,
},
{
title: 'Custom Styling',
code: ``,
},
],
};
const usageGuidelines = {
title: 'Progress Usage Guidelines',
description:
'Best practices for implementing progress indicators effectively.',
guidelines: [
{
title: 'When to Use Progress',
items: [
'File uploads, downloads, or data transfers',
'Multi-step processes or forms',
'Loading operations with known duration',
'Skill levels or completion percentages',
'System resource usage (storage, memory)',
],
},
{
title: 'Progress Types',
items: [
'Determinate: Use when you know the total and current progress',
'Indeterminate: Use for unknown durations or when progress cannot be measured',
'Buffering: Show both loaded and buffered content (video players)',
'Stepped: Discrete progress through defined stages',
],
},
{
title: 'Visual Design',
items: [
'Use appropriate height: thin for subtle progress, thick for prominent indicators',
'Choose colors that match the context (success green, warning yellow, error red)',
'Consider animation for smooth visual feedback',
'Provide clear labels showing current state and percentage when helpful',
'Ensure sufficient contrast for accessibility',
],
},
{
title: 'User Experience',
items: [
'Always provide feedback during long operations',
'Show percentage or time estimates when possible',
'Allow users to cancel or pause lengthy operations',
'Use progress bars consistently across similar operations',
'Consider showing multiple progress indicators for complex operations',
],
},
{
title: 'Accessibility',
items: [
'Progress elements are automatically announced by screen readers',
'Provide meaningful aria-label or aria-labelledby attributes',
'Use role="progressbar" for semantic clarity',
'Include text alternatives for purely visual progress indicators',
'Ensure progress updates are announced to assistive technologies',
],
},
],
};
export default function ProgressStory() {
const [controls, setControls] = useState({
value: 65,
max: 100,
className: '',
size: 'default' as 'sm' | 'default' | 'lg',
variant: 'default' as 'default' | 'success' | 'warning' | 'error',
animated: true,
showLabel: true,
indeterminate: false,
});
const generateCode = () => {
const displayValue = controls.indeterminate ? undefined : controls.value;
const sizeClass = sizeClasses[controls.size];
const variantClass = variantClasses[controls.variant];
const animationClass = controls.indeterminate
? 'animate-pulse [&>*]:animate-pulse [&>*]:w-1/3 [&>*]:translate-x-0'
: '';
const transitionClass =
controls.animated && !controls.indeterminate
? 'transition-all duration-500 ease-out'
: '';
const className = [
sizeClass,
variantClass,
animationClass,
transitionClass,
controls.className,
]
.filter(Boolean)
.join(' ');
const propsString = generatePropsString(
{
value: displayValue,
max: controls.max !== 100 ? controls.max : undefined,
className: className || undefined,
},
{
value: undefined,
max: 100,
className: '',
},
);
const importStatement = generateImportStatement(
['Progress'],
'@kit/ui/progress',
);
const progressComponent = ``;
let fullExample = progressComponent;
// Add label wrapper if showLabel is enabled
if (controls.showLabel) {
const percentage = controls.indeterminate
? 0
: Math.round((displayValue! / controls.max) * 100);
const labelText = controls.indeterminate
? 'Loading...'
: `${percentage}%`;
fullExample = `
Progress
${labelText}
${progressComponent}
`;
}
return `${importStatement}
${fullExample}`;
};
const controlsContent = (
setControls((prev) => ({ ...prev, value }))
}
disabled={controls.indeterminate}
/>
0
{controls.value}
{controls.max}
setControls((prev) => ({ ...prev, max }))}
/>
50
{controls.max}
200
setControls((prev) => ({ ...prev, animated: checked }))
}
/>
setControls((prev) => ({ ...prev, showLabel: checked }))
}
/>
setControls((prev) => ({ ...prev, indeterminate: checked }))
}
/>
setControls((prev) => ({ ...prev, className: e.target.value }))
}
placeholder="Additional CSS classes"
/>
);
const previewContent = (
setControls((prev) => ({ ...prev, value }))}
onMaxChange={(max) => setControls((prev) => ({ ...prev, max }))}
onClassNameChange={(className) =>
setControls((prev) => ({ ...prev, className }))
}
onSizeChange={(size) => setControls((prev) => ({ ...prev, size }))}
onVariantChange={(variant) =>
setControls((prev) => ({ ...prev, variant }))
}
onAnimatedChange={(animated) =>
setControls((prev) => ({ ...prev, animated }))
}
onShowLabelChange={(showLabel) =>
setControls((prev) => ({ ...prev, showLabel }))
}
onIndeterminateChange={(indeterminate) =>
setControls((prev) => ({ ...prev, indeterminate }))
}
/>
);
return (
{examples.map((example, index) => (
{example.title}
{example.description}
))}
}
apiReference={
{apiReference.title}
{apiReference.description}
| Prop |
Type |
Default |
Description |
{apiReference.props.map((prop, index) => (
| {prop.name} |
{prop.type} |
{(prop as any).default || '-'} |
{prop.description} |
))}
Code Examples
{apiReference.examples.map((example, index) => (
))}
}
usageGuidelines={
{usageGuidelines.title}
{usageGuidelines.description}
{usageGuidelines.guidelines.map((section, index) => (
{section.title}
{section.items.map((item, itemIndex) => (
-
{item}
))}
))}
}
/>
);
}