import { Accessor, createContext, createSignal, JSX, Setter, useContext, } from "solid-js"; export interface StepBase { id: string; } export type Step = StepBase & ExtraFields; export interface StepOptions { initialStep: Id; } export function createStepper< T extends readonly Step[], StepId extends T[number]["id"], Extra = unknown, >(s: { steps: T }, stepOpts: StepOptions): StepperReturn { const [activeStep, setActiveStep] = createSignal( stepOpts.initialStep, ); /** * Hooks to manage the current step in the workflow. * It provides the active step and a function to set the active step. */ return { activeStep, setActiveStep, currentStep: () => { const curr = s.steps.find((step) => step.id === activeStep()); if (!curr) { throw new Error(`Step with id ${activeStep()} not found`); } return curr; }, next: () => { const currentIndex = s.steps.findIndex( (step) => step.id === activeStep(), ); if (currentIndex === -1 || currentIndex === s.steps.length - 1) { throw new Error("No next step available"); } setActiveStep(s.steps[currentIndex + 1].id); }, previous: () => { const currentIndex = s.steps.findIndex( (step) => step.id === activeStep(), ); if (currentIndex <= 0) { throw new Error("No previous step available"); } setActiveStep(s.steps[currentIndex - 1].id); }, hasPrevious: () => { const currentIndex = s.steps.findIndex( (step) => step.id === activeStep(), ); return currentIndex > 0; }, hasNext: () => { const currentIndex = s.steps.findIndex( (step) => step.id === activeStep(), ); return currentIndex >= 0 && currentIndex < s.steps.length - 1; }, }; } export interface StepperReturn< T extends readonly Step[], StepId = T[number]["id"], > { activeStep: Accessor; setActiveStep: Setter; currentStep: () => T[number]; next: () => void; previous: () => void; hasPrevious: () => boolean; hasNext: () => boolean; } const StepperContext = createContext(); // Concrete type will be provided by the provider // Default assignment to "never" forces users to specify the type when using the hook, otherwise the return type will be `never`. export function useStepper() { const ctx = useContext(StepperContext); if (!ctx) throw new Error("useStepper must be used inside StepperProvider"); return ctx as T extends never ? never : StepperReturn; // type casting required due to context limitations } interface ProviderProps { stepper: StepperReturn; children: JSX.Element; } interface ProviderProps< T extends readonly Step[], StepId extends T[number]["id"], > { stepper: StepperReturn; children: JSX.Element; } export function StepperProvider< T extends readonly Step[], StepId extends T[number]["id"], >(props: ProviderProps) { return ( {props.children} ); } /** * Helper function to define steps in a type-safe manner. */ export function defineSteps(steps: T) { return steps; }