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.
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.jsonSource
"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.