Step By Step

Guide d'etapes numerotees avec suivi interactif optionnel de la progression. Installez Step By Step depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Guide d'etapes numerotees avec suivi interactif optionnel de la progression. Fait partie de la famille learning dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Step By Step a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/step-by-step.json

Source

"use client"; import { useState } from "react"; import type { ReactNode } from "react"; import type { HeadingTag } from "../../lib/types"; import { cn } from "../../lib/utils"; export type StepProps = { children: ReactNode; className?: string; number?: number; title: string; }; function Step({ children, className, number, title, }: StepProps): React.ReactNode { return ( <div className={cn("flex gap-4", className)}> <div className="flex flex-col items-center"> <div className="flex size-8 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold"> {number} </div> <div className="w-px flex-1 bg-border mt-2" /> </div> <div className="flex-1 pb-8 last:pb-0"> <h4 className="font-semibold text-foreground mb-2">{title}</h4> <div className="text-sm text-muted-foreground [&>p]:mb-2 [&>pre]:my-2"> {children} </div> </div> </div> ); } type InteractiveStepProps = { children: ReactNode; isCompleted: boolean; isLast: boolean; onToggle: () => void; stepNumber: number; title: string; }; function InteractiveStep({ children, isCompleted, isLast, onToggle, stepNumber, title, }: InteractiveStepProps): React.ReactNode { return ( <div className="flex gap-4"> <div className="flex flex-col items-center"> <button className={cn( "flex size-8 items-center justify-center rounded-full text-sm font-bold transition-colors", isCompleted ? "bg-green-500 text-white" : "bg-primary text-primary-foreground", )} onClick={onToggle} type="button" > {isCompleted ? ( <svg className="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} /> </svg> ) : ( stepNumber )} </button> {!isLast && ( <div className={cn( "w-px flex-1 mt-2", isCompleted ? "bg-green-500" : "bg-border", )} /> )} </div> <div className={cn( "flex-1 pb-8 transition-opacity", isCompleted && "opacity-60", )} > <h4 className={cn( "font-semibold text-foreground mb-2", isCompleted && "line-through", )} > {title} </h4> <div className="text-sm text-muted-foreground [&>p]:mb-2 [&>pre]:my-2"> {children} </div> </div> </div> ); } export type StepByStepProps = { /** Heading tag for the main title. Defaults to `h3`. */ as?: HeadingTag; children: ReactNode; className?: string; interactive?: boolean; title?: string; }; // eslint-disable-next-line max-lines-per-function -- Complex component with interactive/non-interactive modes function StepByStep({ as: Heading = "h3", children, className, interactive = false, title, }: StepByStepProps): React.ReactNode { const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set()); const steps = Array.isArray(children) ? children : [children]; const toggleStep = (index: number): void => { const newCompleted = new Set(completedSteps); if (newCompleted.has(index)) newCompleted.delete(index); else newCompleted.add(index); setCompletedSteps(newCompleted); }; if (!interactive) { return ( <div className={cn("my-6", className)}> {title ? ( <div className="flex items-center gap-2 mb-4"> <svg className="size-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} /> </svg> <Heading className="font-semibold text-lg">{title}</Heading> </div> ) : null} <div className="space-y-0"> {steps.map((step, index) => { const stepElement = step as React.ReactElement<StepProps>; const stepKey = `${stepElement.props.title}-${index + 1}`; return ( <Step key={stepKey} number={index + 1} title={stepElement.props.title} > {stepElement.props.children} </Step> ); })} </div> </div> ); } return ( <div className={cn("my-6", className)}> {title ? ( <div className="flex items-center gap-2 mb-4"> <svg className="size-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} /> </svg> <h3 className="font-semibold text-lg">{title}</h3> <span className="text-xs text-muted-foreground ml-auto"> {completedSteps.size}/{steps.length} completed </span> </div> ) : null} <div className="space-y-0"> {steps.map((step, index) => ( <InteractiveStep isCompleted={completedSteps.has(index)} isLast={index === steps.length - 1} key={`${(step as React.ReactElement<StepProps>).props.title}-${index + 1}`} onToggle={() => { toggleStep(index); }} stepNumber={index + 1} title={(step as React.ReactElement<StepProps>).props.title} > {(step as React.ReactElement<StepProps>).props.children} </InteractiveStep> ))} </div> </div> ); } StepByStep.Step = Step; export { Step, StepByStep };

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0