import * as React from "react"; import { Check } from "lucide-react"; import { cn } from "@/lib/utils"; /** * Stepper — WealthX Design System * * Built from scratch (no Base UI primitive). Supports horizontal and vertical * orientations with active, completed, and error step states. * * Figma: https://www.figma.com/design/9V9F0NGVsif8LGmEhVjOcT/Design-System---shadcn * * Composition: * * * ... * * * Vertical with expandable content: * * *
* * Label *
* ... *
*
*/ // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- interface StepperContextValue { activeStep: number; orientation: "horizontal" | "vertical"; totalSteps: number; } const StepperContext = React.createContext({ activeStep: 0, orientation: "horizontal", totalSteps: 0, }); interface StepContextValue { index: number; isActive: boolean; isCompleted: boolean; isFirst: boolean; isLast: boolean; } const StepContext = React.createContext({ index: 0, isActive: false, isCompleted: false, isFirst: true, isLast: false, }); // --------------------------------------------------------------------------- // Stepper // --------------------------------------------------------------------------- export interface StepperProps { /** 0-based index of the currently active step. */ activeStep?: number; orientation?: "horizontal" | "vertical"; className?: string; children: React.ReactNode; } function Stepper({ activeStep = 0, orientation = "horizontal", className, children, }: StepperProps) { const totalSteps = React.Children.count(children); return (
{React.Children.map(children, (step, index) => React.isValidElement(step) ? React.cloneElement( step as React.ReactElement, { _index: index } as Partial, ) : step, )}
); } // --------------------------------------------------------------------------- // Step // --------------------------------------------------------------------------- export interface StepProps { /** * Injected by `` via `React.cloneElement` — do not pass manually. * @internal */ _index?: number; /** Mark this step as errored regardless of active state. */ error?: boolean; className?: string; children: React.ReactNode; } function Step({ _index = 0, error, className, children }: StepProps) { const { activeStep, orientation, totalSteps } = React.useContext(StepperContext); const isActive = _index === activeStep; const isCompleted = _index < activeStep; const isFirst = _index === 0; const isLast = _index === totalSteps - 1; return (
{children}
); } // --------------------------------------------------------------------------- // StepIndicator — circle + connector lines // --------------------------------------------------------------------------- export interface StepIndicatorProps { /** Pass `true` to render the circle in the error/destructive state. */ error?: boolean; className?: string; } function StepIndicator({ error, className }: StepIndicatorProps) { const { orientation } = React.useContext(StepperContext); const { index, isActive, isCompleted, isFirst, isLast } = React.useContext(StepContext); const filled = isActive || isCompleted; const circle = (
{isCompleted && !error ? ( ) : ( {index + 1} )}
); if (orientation === "horizontal") { return (
{circle}
); } return (
{circle} {!isLast && (
)}
); } // --------------------------------------------------------------------------- // StepLabel // --------------------------------------------------------------------------- export interface StepLabelProps { children: React.ReactNode; description?: string; /** Pass a string to show a custom error message, or `true` to show description in error colour. */ error?: string | boolean; className?: string; } function StepLabel({ children, description, error, className, }: StepLabelProps) { const { orientation } = React.useContext(StepperContext); const { isActive, isCompleted, isLast } = React.useContext(StepContext); const subtext = typeof error === "string" ? error : description; return (
{children} {subtext && ( {subtext} )}
); } // --------------------------------------------------------------------------- // StepContent — visible only on the active step (vertical orientation) // --------------------------------------------------------------------------- export interface StepContentProps { className?: string; children: React.ReactNode; } function StepContent({ className, children }: StepContentProps) { const { isActive } = React.useContext(StepContext); if (!isActive) return null; return (
{children}
); } // --------------------------------------------------------------------------- // StepItem — convenience wrapper for horizontal steppers // Renders StepIndicator + StepLabel in a single element. // --------------------------------------------------------------------------- export interface StepItemProps { label: string; description?: string; error?: string | boolean; } function StepItem({ label, description, error }: StepItemProps) { return ( <> {label} ); } export { Stepper, Step, StepIndicator, StepLabel, StepContent, StepItem };