Skip to content

Instantly share code, notes, and snippets.

@zaknesler
Last active September 8, 2022 21:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zaknesler/7fba361fe39199edc43962f61c6611d8 to your computer and use it in GitHub Desktop.
Save zaknesler/7fba361fe39199edc43962f61c6611d8 to your computer and use it in GitHub Desktop.
import { CheckCircleIcon } from '@heroicons/react/20/solid'
import React, { HTMLAttributes, ReactNode } from 'react'
import { cx } from '../utils'
const variants = new Map<
'primary' | 'secondary',
{ text: string; done: string; active: string; inactive: string; line: string }
>([
[
'primary',
{
text: 'text-accent-700',
done: 'fill-accent-600',
active: 'bg-amber-600 ring-amber-600',
inactive: 'bg-brand-300',
line: 'bg-gray-400',
},
],
[
'secondary',
{
text: 'text-brand-300',
done: 'fill-brand-200',
active: 'bg-amber-200 ring-amber-200',
inactive: 'bg-brand-400',
line: 'bg-white',
},
],
])
const spacings = new Map<
'xs' | 'sm' | 'md' | 'lg',
{ gap: string; height: string }
>([
['xs', { gap: 'gap-0.5', height: 'min-h-[0.5rem]' }],
['sm', { gap: 'gap-1', height: 'min-h-[0.75rem]' }],
['md', { gap: 'gap-2', height: 'min-h-[1rem]' }],
['lg', { gap: 'gap-2', height: 'min-h-[1.5rem]' }],
])
type ProgressProps = HTMLAttributes<HTMLDivElement> & {
steps: (ReactNode | ReactNode[])[]
descriptions?: ReactNode[]
activeIndex: number
variant?: 'primary' | 'secondary'
spacing?: 'xs' | 'sm' | 'md' | 'lg'
showStepCount?: boolean
showLines?: boolean
}
export const Progress: React.FC<ProgressProps> = ({
steps,
descriptions,
activeIndex,
variant = 'primary',
spacing = 'sm',
showStepCount = false,
showLines = false,
...props
}) => {
const v = variants.get(variant)!
const s = spacings.get(spacing)!
const isActive = (index: number) => index === activeIndex
const isDone = (index: number) => index < activeIndex
const isPending = (index: number) => index > activeIndex
const hasTextToDisplay = (index: number) =>
showStepCount || descriptions?.[index]
const renderElement = (element: ReactNode | ReactNode[], index: number) => {
if (!Array.isArray(element)) return element
if (element.length === 2 && isDone(index)) return element[1]
return element[0]
}
const icon = (index: number) => {
if (isDone(index))
return (
<CheckCircleIcon
className={cx(['-ml-1.5 h-5 w-5 animate-scaleIn', v.text])}
/>
)
return (
<div
className={cx([
'mr-1.5 h-2 w-2 rounded-full',
isActive(index)
? cx(['animate-pulse ring-4 ring-opacity-30', v.active])
: v.inactive,
])}
/>
)
}
const line = (index: number, element: ReactNode | ReactNode[]) => {
if (showStepCount)
return (
<div
className={cx([
'ml-1.5 transition-opacity',
v.text,
isPending(index) ? 'opacity-60' : 'opacity-100',
])}
>
Step {index + 1}
</div>
)
return (
<div
className={cx([
'ml-1.5 leading-tight transition-opacity',
isPending(index) ? 'opacity-60' : 'opacity-100',
])}
>
{renderElement(element, index)}
</div>
)
}
const lineGap = (index: number, element: ReactNode | ReactNode[]) => {
if (showLines && index === steps.length - 1) return null
return (
<div className="flex items-stretch">
<div
className={cx([
'w-4 flex-shrink-0',
hasTextToDisplay(index) ? 'min-h-[2rem]' : s.height,
])}
>
{showLines && index < steps.length - 1 && (
<div
className={cx([
'mx-auto h-full w-[2px] rounded-full',
v.line,
isDone(index) ? 'opacity-50' : 'opacity-20',
])}
/>
)}
</div>
<div className="ml-2 self-start">
{hasTextToDisplay(index) && (
<div
className={cx([
'flex flex-col leading-tight transition-opacity',
descriptions?.[index] && 'font-normal text-gray-500',
isPending(index) ? 'opacity-60' : 'opacity-100',
])}
>
{descriptions?.[index] || renderElement(element, index)}
</div>
)}
</div>
</div>
)
}
return (
<div
{...props}
className={cx(['flex flex-col flex-wrap', props.className, s.gap])}
>
{steps.map((element, index) => (
<div key={index} className={cx(['flex flex-col font-semibold', s.gap])}>
<div className="ml-1 flex items-center">
{icon(index)}
{line(index, element)}
</div>
{lineGap(index, element)}
</div>
))}
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment