Implement stepper and progress bar

This commit is contained in:
adilallo
2026-02-02 11:24:47 -07:00
parent 9149883ae0
commit 462488ddce
13 changed files with 711 additions and 0 deletions
+128
View File
@@ -4,6 +4,8 @@ import { useState } from "react";
import Tooltip from "../components/Tooltip";
import Alert from "../components/Alert";
import Button from "../components/Button";
import Stepper from "../components/Stepper";
import Progress from "../components/Progress";
export default function ComponentsPreview() {
const [alertVisible, setAlertVisible] = useState({
@@ -178,6 +180,132 @@ export default function ComponentsPreview() {
</div>
</div>
</section>
{/* Stepper Section */}
<section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Stepper Component
</h2>
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
<div className="space-y-[var(--spacing-scale-016)]">
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Step 1 of 5
</p>
<Stepper active={1} totalSteps={5} />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Step 2 of 5
</p>
<Stepper active={2} totalSteps={5} />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Step 3 of 5
</p>
<Stepper active={3} totalSteps={5} />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Step 4 of 5
</p>
<Stepper active={4} totalSteps={5} />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Step 5 of 5
</p>
<Stepper active={5} totalSteps={5} />
</div>
</div>
</div>
</section>
{/* Progress Section */}
<section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Progress Component
</h2>
<div className="bg-white rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
<div className="space-y-[var(--spacing-scale-016)]">
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-0
</p>
<Progress progress="1-0" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-1
</p>
<Progress progress="1-1" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-2
</p>
<Progress progress="1-2" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-3
</p>
<Progress progress="1-3" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-4
</p>
<Progress progress="1-4" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 1-5
</p>
<Progress progress="1-5" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 2-0
</p>
<Progress progress="2-0" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 2-1
</p>
<Progress progress="2-1" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 2-2
</p>
<Progress progress="2-2" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 3-0
</p>
<Progress progress="3-0" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 3-1
</p>
<Progress progress="3-1" />
</div>
<div>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
Progress: 3-2
</p>
<Progress progress="3-2" />
</div>
</div>
</div>
</section>
</div>
</div>
);
@@ -0,0 +1,23 @@
"use client";
import { memo } from "react";
import { ProgressView } from "./Progress.view";
import type { ProgressProps } from "./Progress.types";
const ProgressContainer = memo<ProgressProps>(
({ progress = "3-2", className = "" }) => {
const barClasses = `h-[8px] relative w-full`;
return (
<ProgressView
progress={progress}
className={className}
barClasses={barClasses}
/>
);
},
);
ProgressContainer.displayName = "Progress";
export default ProgressContainer;
+24
View File
@@ -0,0 +1,24 @@
export type ProgressBarState =
| "1-0"
| "1-1"
| "1-2"
| "1-3"
| "1-4"
| "1-5"
| "2-0"
| "2-1"
| "2-2"
| "3-0"
| "3-1"
| "3-2";
export interface ProgressProps {
progress?: ProgressBarState;
className?: string;
}
export interface ProgressViewProps {
progress: ProgressBarState;
className: string;
barClasses: string;
}
+80
View File
@@ -0,0 +1,80 @@
import type { ProgressViewProps } from "./Progress.types";
export function ProgressView({
progress,
className,
barClasses,
}: ProgressViewProps) {
// Progress bar type
const [fullSegments, partialSegment] = progress.split("-").map(Number);
// Calculate total progress:
// - For 1-X: first section is (X+1)/6 filled
// - For 2-X: first section full, second section X/3 filled
// - For 3-X: first two sections full, third section X/3 filled
// Max is 3 full segments = 9 units
let totalProgress = 0;
if (fullSegments === 1) {
totalProgress = (partialSegment + 1) / 6; // 1/6 to 6/6 of first section
} else if (fullSegments === 2) {
totalProgress = 1 + partialSegment / 3; // 1 full + 0/3 to 2/3 of second
} else if (fullSegments === 3) {
totalProgress = 2 + partialSegment / 3; // 2 full + 0/3 to 2/3 of third
}
const maxProgress = 3;
const progressPercentage = Math.round((totalProgress / maxProgress) * 100);
return (
<div
className={`${barClasses} ${className}`}
role="progressbar"
aria-valuenow={totalProgress}
aria-valuemin={0}
aria-valuemax={3}
aria-label={`Progress: ${progressPercentage}%`}
>
{/* Background layer - 3 segments */}
<div className="absolute inset-0 flex gap-[var(--spacing-scale-008)] px-[4px]">
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)] rounded-l-[var(--radius-full)]" />
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)]" />
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)] rounded-r-[var(--radius-full)]" />
</div>
{/* Fill layer - always show 3 sections, fill amount varies */}
<div className="absolute inset-0 flex gap-[var(--spacing-scale-008)] px-[4px] overflow-hidden">
{/* First section - for 1-X: (X+1)/6 filled, for 2-X and 3-X: fully filled */}
<div className="flex-1 h-full relative">
{fullSegments === 1 ? (
<div
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]"
style={{ width: `${((partialSegment + 1) / 6) * 100}%` }}
/>
) : fullSegments >= 2 ? (
<div className="absolute inset-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]" />
) : null}
</div>
{/* Second section - for 2-X: X/3 filled, for 3-X: fully filled, otherwise empty */}
<div className="flex-1 h-full relative">
{fullSegments === 2 ? (
partialSegment > 0 ? (
<div
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)]"
style={{ width: `${(partialSegment / 3) * 100}%` }}
/>
) : null
) : fullSegments >= 3 ? (
<div className="absolute inset-0 bg-[var(--color-content-default-brand-primary)]" />
) : null}
</div>
{/* Third section - for 3-X: X/3 filled, otherwise empty */}
<div className="flex-1 h-full relative">
{fullSegments === 3 && partialSegment > 0 ? (
<div
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)]"
style={{ width: `${(partialSegment / 3) * 100}%` }}
/>
) : null}
</div>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { default } from "./Progress.container";
@@ -0,0 +1,24 @@
"use client";
import { memo } from "react";
import { StepperView } from "./Stepper.view";
import type { StepperProps } from "./Stepper.types";
const StepperContainer = memo<StepperProps>(
({ active = 1, totalSteps = 5, className = "" }) => {
const stepperClasses = `flex gap-[var(--spacing-scale-012)] items-center relative`;
return (
<StepperView
active={active}
totalSteps={totalSteps}
className={className}
stepperClasses={stepperClasses}
/>
);
},
);
StepperContainer.displayName = "Stepper";
export default StepperContainer;
+14
View File
@@ -0,0 +1,14 @@
export type StepperActive = 1 | 2 | 3 | 4 | 5;
export interface StepperProps {
active?: StepperActive;
totalSteps?: number;
className?: string;
}
export interface StepperViewProps {
active: StepperActive;
totalSteps: number;
className: string;
stepperClasses: string;
}
+50
View File
@@ -0,0 +1,50 @@
import type { StepperViewProps } from "./Stepper.types";
export function StepperView({
active,
totalSteps,
className,
stepperClasses,
}: StepperViewProps) {
return (
<div
className={`${stepperClasses} ${className}`}
role="progressbar"
aria-valuenow={active}
aria-valuemin={1}
aria-valuemax={totalSteps}
aria-label={`Step ${active} of ${totalSteps}`}
>
{Array.from({ length: totalSteps }, (_, index) => {
const stepNumber = index + 1;
const isActive = stepNumber === active;
return (
<div
key={stepNumber}
className="shrink-0 w-[12px] h-[12px] flex items-center justify-center"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={isActive ? "opacity-100" : "opacity-30"}
>
<circle
cx="6"
cy="6"
r="5.5"
fill="var(--color-surface-inverse-secondary)"
stroke="var(--color-surface-inverse-secondary)"
strokeWidth="1"
/>
</svg>
</div>
);
})}
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { default } from "./Stepper.container";