111 lines
5.0 KiB
TypeScript
111 lines
5.0 KiB
TypeScript
import type { ProportionBarViewProps } from "./ProportionBar.types";
|
|
|
|
export function ProportionBarView({
|
|
progress,
|
|
className,
|
|
barClasses,
|
|
variant,
|
|
}: ProportionBarViewProps) {
|
|
// Proportion bar type
|
|
const [fullSegments, partialSegment] = progress.split("-").map(Number);
|
|
const segmented = variant === "segmented";
|
|
// 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);
|
|
// Check if at 100% completion (all 3 segments fully filled)
|
|
// Note: Current type system max is "3-2" (88.9%), true 100% would require "3-3" or all segments fully filled
|
|
const isFull = totalProgress >= maxProgress;
|
|
// Generate descriptive aria-label
|
|
const ariaLabelText = isFull
|
|
? "Progress: Complete (100%)"
|
|
: fullSegments === 3 && partialSegment === 2
|
|
? `Progress: ${fullSegments} segments complete, maximum state (${progressPercentage}%)`
|
|
: fullSegments === 3
|
|
? `Progress: ${fullSegments} segments, ${partialSegment} of 3 parts filled (${progressPercentage}%)`
|
|
: fullSegments === 2
|
|
? `Progress: ${fullSegments} segments, ${partialSegment} of 3 parts filled (${progressPercentage}%)`
|
|
: `Progress: ${fullSegments} segment, ${partialSegment + 1} of 6 parts filled (${progressPercentage}%)`;
|
|
|
|
return (
|
|
<div
|
|
className={`${barClasses} ${className}`}
|
|
role="progressbar"
|
|
aria-valuenow={totalProgress}
|
|
aria-valuemin={0}
|
|
aria-valuemax={3}
|
|
aria-label={ariaLabelText}
|
|
>
|
|
{/* 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)] ${
|
|
segmented && partialSegment < 5
|
|
? "rounded-r-[var(--radius-full)]"
|
|
: ""
|
|
}`.trim()}
|
|
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)] ${
|
|
segmented
|
|
? "rounded-l-[var(--radius-full)] rounded-r-[var(--radius-full)]"
|
|
: ""
|
|
}`.trim()}
|
|
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 */}
|
|
{/* Round right corner when at 100% (third section fully filled, partialSegment === 3) */}
|
|
<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)] ${
|
|
segmented
|
|
? "rounded-l-[var(--radius-full)] rounded-r-[var(--radius-full)]"
|
|
: partialSegment >= 3
|
|
? "rounded-r-[var(--radius-full)]"
|
|
: ""
|
|
}`.trim()}
|
|
style={{ width: `${Math.min((partialSegment / 3) * 100, 100)}%` }}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|