rubis/apps/web/src/components/ui/Stepper.tsx
ordinarthur 332bf0bcda feat(web): /signup + 3-step onboarding flow
Nouvelles routes :
- /signup : inscription (fullName + email + password) → /onboarding/compte
- /onboarding : layout avec brand + stepper, auth-guard
- /onboarding/compte : étape 1 (nom + email, prefilled depuis la session)
- /onboarding/entreprise : étape 2 (nom, SIRET optionnel, chips volume)
- /onboarding/signature : étape 3 (signature email + aperçu live)

Nouvelles primitives UI :
- <Card variant="default|flat|hero" padding="sm|md|lg">
- <Stepper> wizard horizontal (current rubis, done rubis-glow + ✓, todo line)
- <Chip selected> : pastille pill, glow + deep quand sélectionnée (le rubis
  plein reste réservé aux CTA, cf. règle "le rubis est rare")
- <Textarea> : mêmes règles a11y/focus que <Input>

MSW handlers étendus :
- PATCH /api/v1/account/profile (fullName, email, signature)
- PATCH /api/v1/organizations/me (name, siret, monthlyVolumeBucket)
- mockDb : ajout des organizations, méthodes updateUser/updateOrg

Wiring :
- /login → "Créer un compte" pointe vers /signup (avant : loop)
- /login succès → /  (au lieu de /login)
- /  → /onboarding/compte si auth, /login sinon (placeholder dashboard)
- /onboarding/signature succès → /

Bundle prod : 113.87 KB gzip core (-2 KB grâce à MSW exclu en prod via
import.meta.env.DEV). Chaque route en chunk dédié (1-2 KB gzip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:22:53 +02:00

85 lines
3.0 KiB
TypeScript

import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
/**
* Stepper horizontal pour wizards multi-étapes (onboarding).
* - Étape courante : cercle plein rubis, label rubis
* - Étape complétée : cercle rubis-glow + ✓ rubis-deep
* - Étape future : cercle vide bordure line, label muted
* - Lignes entre les cercles : rubis si la précédente est complétée
*
* Pas une UI standard "1—2—3 cliquable". Les étapes ne sont pas navigables :
* c'est un wizard linéaire (cf. /docs/produit.md).
*/
type Step = {
/** Identifiant stable pour `key`. */
id: string;
label: string;
};
type StepperProps = {
steps: ReadonlyArray<Step>;
/** Index 0-based de l'étape courante. */
currentIndex: number;
className?: string;
};
export function Stepper({ steps, currentIndex, className }: StepperProps) {
return (
<ol
className={cn("flex items-center gap-2 sm:gap-3", className)}
aria-label={`Étape ${currentIndex + 1} sur ${steps.length}`}
>
{steps.map((step, idx) => {
const status: "done" | "current" | "todo" =
idx < currentIndex ? "done" : idx === currentIndex ? "current" : "todo";
const isLast = idx === steps.length - 1;
return (
<li key={step.id} className="flex items-center gap-2 sm:gap-3 flex-1">
<div className="flex flex-col items-center gap-2">
<span
aria-current={status === "current" ? "step" : undefined}
className={cn(
"flex size-8 items-center justify-center rounded-full text-[13px] font-semibold",
"transition-[background,border-color,color] duration-200",
status === "current" &&
"bg-rubis text-white shadow-rubis tabular-nums",
status === "done" &&
"bg-rubis-glow text-rubis-deep tabular-nums",
status === "todo" &&
"border border-line bg-white text-ink-3 tabular-nums",
)}
>
{status === "done" ? (
<Check size={15} aria-hidden="true" strokeWidth={2.5} />
) : (
idx + 1
)}
</span>
<span
className={cn(
"hidden sm:block text-[11px] font-semibold uppercase tracking-[0.1em] whitespace-nowrap",
status === "current" && "text-rubis",
status === "done" && "text-ink-2",
status === "todo" && "text-ink-3",
)}
>
{step.label}
</span>
</div>
{!isLast && (
<span
aria-hidden="true"
className={cn(
"h-px flex-1 transition-colors duration-200",
status === "done" ? "bg-rubis" : "bg-line",
)}
/>
)}
</li>
);
})}
</ol>
);
}