️ feat: implement steps/ timeline

This commit is contained in:
Andre H 2024-03-04 10:38:12 +08:00
parent adb64f2db1
commit 6c42a22972
6 changed files with 167 additions and 0 deletions

View File

@ -0,0 +1,43 @@
import { VariantProps, tv } from 'tailwind-variants';
export const stepTheme = tv({
slots: {
wrapper: ['relative', 'px-1.5', 'py-1.5', 'flex', 'gap-2', 'items-center'],
step: [
'bg-base-bg-emphasized',
'rounded-full',
'w-7',
'h-7',
'flex',
'items-center',
'justify-center',
'text-elements-mid-em',
'shadow-button',
'shrink-0',
],
label: ['text-sm', 'font-sans', 'text-elements-mid-em'],
connector: ['bg-border-interactive-hovered'],
},
variants: {
orientation: {
vertical: { connector: ['w-px', 'h-3', 'ml-5'] },
horizontal: { connector: ['h-px', 'w-full'] },
},
active: {
true: {
step: ['bg-controls-secondary-hovered', 'text-elements-on-secondary'],
label: ['text-elements-high-em'],
},
},
completed: {
true: {
step: ['text-controls-primary'],
},
},
},
defaultVariants: {
orientation: 'vertical',
},
});
export type StepTheme = VariantProps<typeof stepTheme>;

View File

@ -0,0 +1,60 @@
import React, { useCallback, ComponentPropsWithoutRef } from 'react';
import { stepTheme, StepTheme } from './Step.theme';
import { CheckRoundFilledIcon } from 'components/shared/CustomIcon';
export interface StepProps extends ComponentPropsWithoutRef<'li'>, StepTheme {
/**
* The label for the step
*/
label: string;
/**
* The index of the step
*/
index: number;
/**
* The total number of steps
*/
currentIndex: number;
}
export const Step = ({
label,
index,
currentIndex,
orientation,
...props
}: StepProps) => {
const theme = stepTheme();
const active = currentIndex === index;
const completed = currentIndex > index;
const renderConnector = useCallback(
(index: number) => {
if (index === 0) {
return null;
}
return <div aria-hidden className={theme.connector({ orientation })} />;
},
[theme],
);
return (
<>
{renderConnector(index)}
<li className={theme.wrapper()} {...props}>
{
<div className={theme.step({ active, completed })}>
{completed ? (
<CheckRoundFilledIcon className="w-full h-full" />
) : (
index + 1
)}
</div>
}
<p className={theme.label()}>{label}</p>
</li>
</>
);
};

View File

@ -0,0 +1,2 @@
export * from './Step';
export * from './Step.theme';

View File

@ -0,0 +1,18 @@
import { VariantProps, tv } from 'tailwind-variants';
export const stepsTheme = tv({
slots: {
root: [],
},
variants: {
orientation: {
vertical: { root: ['flex', 'flex-col'] },
horizontal: { root: ['flex', 'items-center'] },
},
},
defaultVariants: {
orientation: 'vertical',
},
});
export type StepsTheme = VariantProps<typeof stepsTheme>;

View File

@ -0,0 +1,42 @@
import React, { Fragment, ComponentPropsWithoutRef } from 'react';
import { stepsTheme, StepsTheme } from './Steps.theme';
import { Step, StepProps, StepTheme } from './Step';
interface StepsProps
extends ComponentPropsWithoutRef<'ul'>,
StepsTheme,
Pick<StepTheme, 'orientation'> {
/**
* The index of the current step
*/
currentIndex: number;
/**
* The steps to render
*/
steps: Pick<StepProps, 'label'>[];
}
export const Steps = ({
currentIndex,
steps = [],
className,
orientation,
...props
}: StepsProps) => {
const theme = stepsTheme();
return (
<ul className={theme.root({ class: className, orientation })} {...props}>
{steps.map((step, i) => (
<Fragment key={i}>
<Step
{...step}
orientation={orientation}
currentIndex={currentIndex}
index={i}
/>
</Fragment>
))}
</ul>
);
};

View File

@ -0,0 +1,2 @@
export * from './Steps';
export * from './Steps.theme';