improvement: treatment plan turned into a wizard. shift+click control added to FDI tooth chart for cunnected prosthesises.
This commit is contained in:
100
frontend/src/components/ui/shared/WizardStepper.tsx
Normal file
100
frontend/src/components/ui/shared/WizardStepper.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
export type WizardStepItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type WizardStepperProps = {
|
||||
steps: WizardStepItem[];
|
||||
currentStepId: string;
|
||||
onStepChange: (stepId: string) => void;
|
||||
/** Accessible name for the stepper. */
|
||||
'aria-label': string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compact horizontal progress stepper — numbered nodes + connector rail.
|
||||
* Distinct from chip/tab row patterns used elsewhere (e.g. treatment detail chips).
|
||||
*/
|
||||
export function WizardStepper({
|
||||
steps,
|
||||
currentStepId,
|
||||
onStepChange,
|
||||
'aria-label': ariaLabel,
|
||||
className,
|
||||
}: WizardStepperProps) {
|
||||
const currentIndex = Math.max(
|
||||
0,
|
||||
steps.findIndex((step) => step.id === currentStepId),
|
||||
);
|
||||
|
||||
return (
|
||||
<nav aria-label={ariaLabel} className={className}>
|
||||
<ol className="flex items-center gap-0 min-w-0">
|
||||
{steps.map((step, index) => {
|
||||
const isCurrent = index === currentIndex;
|
||||
const isComplete = index < currentIndex;
|
||||
const isUpcoming = index > currentIndex;
|
||||
|
||||
return (
|
||||
<li key={step.id} className="flex items-center min-w-0 flex-1 last:flex-none">
|
||||
<button
|
||||
type="button"
|
||||
aria-current={isCurrent ? 'step' : undefined}
|
||||
onClick={() => onStepChange(step.id)}
|
||||
className={`
|
||||
group inline-flex items-center gap-1.5 shrink-0 rounded-[var(--radius-sm)]
|
||||
py-0.5 pe-1 ps-0.5 transition-colors
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
|
||||
${isUpcoming ? 'opacity-70 hover:opacity-100' : ''}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full
|
||||
text-[10px] font-semibold leading-none tabular-nums transition-colors
|
||||
${
|
||||
isCurrent
|
||||
? 'bg-primary text-primary-contrast shadow-[0_0_0_2px_var(--color-primary-soft)]'
|
||||
: isComplete
|
||||
? 'bg-primary-soft text-primary'
|
||||
: 'bg-background-secondary text-text-muted ring-1 ring-inset ring-border'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-[11px] leading-none whitespace-nowrap transition-colors
|
||||
${
|
||||
isCurrent
|
||||
? 'font-semibold text-text-primary'
|
||||
: isComplete
|
||||
? 'font-medium text-text-secondary'
|
||||
: 'font-medium text-text-muted'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{index < steps.length - 1 ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`
|
||||
mx-1.5 h-px min-w-[0.75rem] flex-1 rounded-full
|
||||
${isComplete ? 'bg-primary/55' : 'bg-border'}
|
||||
`}
|
||||
/>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user