mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
197 lines
4.7 KiB
TypeScript
197 lines
4.7 KiB
TypeScript
import classNames from 'classnames';
|
|
import {
|
|
FC,
|
|
MouseEventHandler,
|
|
ReactNode,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { useElemSize } from '@/hooks/layout';
|
|
import { CheckIcon } from '@/components/commons/icon/CheckIcon';
|
|
import { Typography } from '@/components/commons/Typography';
|
|
import { useDebouncedEffect } from '@/hooks/timeout';
|
|
|
|
type StepComponentType = FC<{
|
|
nextStep: () => void;
|
|
prevStep: () => void;
|
|
resetSteps: () => void;
|
|
variant: 'alone' | 'onboarding';
|
|
active: boolean;
|
|
}>;
|
|
export type Step = {
|
|
type: 'numbered' | 'fullsize';
|
|
component: StepComponentType;
|
|
};
|
|
|
|
export function StepContainer({
|
|
children,
|
|
width,
|
|
active,
|
|
type,
|
|
step,
|
|
variant,
|
|
}: {
|
|
type: 'numbered' | 'fullsize';
|
|
variant: 'alone' | 'onboarding';
|
|
children: ReactNode;
|
|
width: number;
|
|
active: boolean;
|
|
step: number;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={classNames(
|
|
'step-container transition-transform duration-500 relative w-full xs:p-8 mobile:p-2 rounded-lg flex gap-4 h-full',
|
|
!active && 'opacity-40 pointer-events-none',
|
|
variant === 'onboarding' && 'bg-background-70',
|
|
variant === 'alone' && 'bg-background-60'
|
|
)}
|
|
style={{
|
|
minWidth: width,
|
|
width,
|
|
}}
|
|
>
|
|
{type === 'numbered' && (
|
|
<div className="xs:flex xs:flex-col mobile:absolute mobile:-top-3 mobile:-right-4">
|
|
<div className="bg-accent-background-40 rounded-full h-8 w-8 flex flex-col items-center justify-center">
|
|
<Typography variant="section-title" bold>
|
|
{step + 1}
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StepDot({
|
|
active,
|
|
done,
|
|
onClick,
|
|
}: {
|
|
active?: boolean;
|
|
done?: boolean;
|
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={classNames(
|
|
'flex h-4 w-4 rounded-full justify-center items-center fill-background-10 transition-all',
|
|
active || done ? 'bg-accent-background-20 ' : 'bg-background-60'
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
{active && <div className="flex h-2 w-2 rounded-full bg-background-10" />}
|
|
{done && <CheckIcon />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StepperSlider({
|
|
variant,
|
|
steps,
|
|
back,
|
|
forward,
|
|
}: {
|
|
variant: 'alone' | 'onboarding';
|
|
steps: Step[];
|
|
/**
|
|
* Ran when step is 0 and `prevStep` is executed
|
|
*/
|
|
back?: () => void;
|
|
/**
|
|
* Ran when step is `steps.length - 1` and nextStep is executed
|
|
*/
|
|
forward?: () => void;
|
|
}) {
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
const { width } = useElemSize(ref);
|
|
const [shouldAnimate, setShouldAnimate] = useState(true);
|
|
const [step, setStep] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setStep((x) => Math.min(x, steps.length - 1));
|
|
}, [steps.length]);
|
|
|
|
const nextStep = () => {
|
|
if (step + 1 === steps.length) {
|
|
forward?.();
|
|
return;
|
|
}
|
|
setStep(step + 1);
|
|
};
|
|
|
|
const prevStep = () => {
|
|
if (step - 1 < 0) {
|
|
back?.();
|
|
return;
|
|
}
|
|
setStep(step - 1);
|
|
};
|
|
|
|
const resetSteps = () => {
|
|
setStep(0);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setShouldAnimate(false);
|
|
}, [width]);
|
|
|
|
// Make it so if you resize the window it wont try to move the slide with an animation
|
|
useDebouncedEffect(
|
|
() => {
|
|
setShouldAnimate(true);
|
|
},
|
|
[width],
|
|
500
|
|
);
|
|
|
|
return (
|
|
<div className="w-full flex flex-col gap-4">
|
|
<div className="w-full flex" ref={ref}>
|
|
<div
|
|
className={classNames('flex gap-8', {
|
|
'transition-transform duration-500 ': shouldAnimate,
|
|
})}
|
|
style={{ transform: `translateX(-${(width + 32) * step}px)` }}
|
|
>
|
|
{steps.map(({ type, component: StepComponent }, index) => (
|
|
<StepContainer
|
|
variant={variant}
|
|
key={index}
|
|
type={type}
|
|
width={width}
|
|
active={index === step}
|
|
step={index}
|
|
>
|
|
<StepComponent
|
|
variant={variant}
|
|
nextStep={nextStep}
|
|
prevStep={prevStep}
|
|
resetSteps={resetSteps}
|
|
active={index === step}
|
|
/>
|
|
</StepContainer>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-center items-center gap-2">
|
|
{Array.from({ length: steps.length }).map((_, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
{index !== 0 && (
|
|
<div className="w-5 h-1 bg-background-50 rounded-full" />
|
|
)}
|
|
<StepDot
|
|
active={index === step}
|
|
done={index < step}
|
|
// onClick={() => setStep(index)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|