feat(billing): redesign page abonnement — layout asymétrique + identité Rubis
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s

Mensuel par défaut dans le toggle (au lieu de yearly).

Layout asymétrique 1fr/1.3fr/1fr avec Pro en hero card centrale, plus
large et plus dense visuellement que Free et Business — sort du pattern
"3 cards égales" générique des landing pages SaaS.

Identité Rubis :
  - Gem 280px en filigrane sur la Pro card (rubis-glow, opacity 70%) —
    le motif signature de la marque, pas un blob de gradient
  - Bullets ◆ losanges rubis tournés (5×5px) au lieu des check-circles
    Lucide génériques — cohérent avec PlanCard, Timeline, etc.
  - Pastille "Le plus pris" avec mini-gem inline sur Pro
  - Border rubis + shadow-card sur Pro, line + sobre pour les voisines
  - Free : bg cream-2 (ton "découverte"), Business : bg blanc + accents
    rubis-deep
  - Strip "plan courant" : remplace le gros bloc card → ligne flex avec
    gem + texte + mini progress bar 32px + bouton ghost. Discret.

Voice direct dans les bénéfices :
  - Header narratif "Trois mois pour voir. Puis vous décidez."
  - "Tester sans poser sa CB" (Free)
  - "Pour les TPE qui n'ont plus jamais à y penser" (Pro titre)
  - "Moins cher qu'une heure de votre temps mensuel" (Pro footer)
  - Annulation 1-clic mentionné en sous-titre du toggle

Prix Pro XL 44px (vs 28px pour les voisines) — hiérarchie visuelle qui
guide l'œil vers l'option qu'on veut pousser.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-07 16:34:46 +02:00
parent 1952265217
commit fd24ef42a6

View File

@ -1,20 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { import { ArrowLeft, ArrowRight, CreditCard } from "lucide-react";
ArrowLeft,
ArrowRight,
Check,
CreditCard,
Sparkles,
Users,
Zap,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle, type PlanKey } from "@/lib/billing"; import { useOpenPortal, useStartCheckout, useSubscription, type BillingCycle } from "@/lib/billing";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { formatDate } from "@/lib/format"; import { formatDate } from "@/lib/format";
import { Gem } from "@/components/brand/Gem";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card"; import { Card } from "@/components/ui/Card";
@ -30,22 +23,13 @@ export const Route = createFileRoute("/_app/parametres_/abonnement")({
component: AbonnementPage, component: AbonnementPage,
}); });
/**
* /parametres/abonnement gestion du plan et de la facturation Rubis.
*
* Sections :
* - Plan actuel + caps + état grace period
* - Comparaison Free / Pro / Business avec toggle mensuel/annuel
* - CTA upgrade (Stripe Checkout) ou portail (Customer Portal)
*/
function AbonnementPage() { function AbonnementPage() {
const search = Route.useSearch(); const search = Route.useSearch();
const { data: sub, isPending } = useSubscription(); const { data: sub, isPending } = useSubscription();
const checkout = useStartCheckout(); const checkout = useStartCheckout();
const portal = useOpenPortal(); const portal = useOpenPortal();
const [cycle, setCycle] = useState<BillingCycle>("yearly"); const [cycle, setCycle] = useState<BillingCycle>("monthly");
// Toast post-redirect Stripe
useEffect(() => { useEffect(() => {
if (search.checkout === "success") { if (search.checkout === "success") {
toast.success("Bienvenue sur le nouveau plan ! Activation en cours…"); toast.success("Bienvenue sur le nouveau plan ! Activation en cours…");
@ -75,8 +59,10 @@ function AbonnementPage() {
}); });
}; };
const currentPlan = sub?.plan;
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-7">
<Link <Link
to="/parametres" to="/parametres"
className="inline-flex items-center gap-1.5 self-start text-[12.5px] text-ink-3 hover:text-rubis" className="inline-flex items-center gap-1.5 self-start text-[12.5px] text-ink-3 hover:text-rubis"
@ -84,71 +70,69 @@ function AbonnementPage() {
<ArrowLeft size={13} aria-hidden="true" /> Paramètres <ArrowLeft size={13} aria-hidden="true" /> Paramètres
</Link> </Link>
<header> {/* Header — direct, pas trop de baratin */}
<header className="max-w-2xl">
<Eyebrow>Abonnement</Eyebrow> <Eyebrow>Abonnement</Eyebrow>
<h1 className="mt-2 font-display text-[28px] font-bold tracking-[-0.022em] text-ink lg:text-[32px]"> <h1 className="mt-2 font-display text-[30px] font-bold tracking-[-0.024em] text-ink lg:text-[36px] leading-[1.05]">
Choisir <em className="text-rubis">son plan</em> Trois mois pour voir.
<br />
Puis vous <em className="text-rubis">décidez</em>.
</h1> </h1>
<p className="mt-1.5 text-[14px] text-ink-3 max-w-2xl leading-relaxed"> <p className="mt-3 text-[14.5px] text-ink-2 leading-relaxed">
Trois mois offerts au démarrage. Au-delà, passez Pro pour Pas de carte demandée pour démarrer. Au-delà de 5 factures actives,
continuer à relancer plus de 5 factures à la fois. vous passez Pro pour continuer quand vous êtes prêt, pas avant.
</p> </p>
</header> </header>
{/* Plan courant */} {/* État courant — slim, info-dense, pas un gros bloc */}
{!isPending && sub && ( {!isPending && sub && (
<CurrentPlanCard <CurrentPlanStrip
state={sub} state={sub}
onOpenPortal={onOpenPortal} onOpenPortal={onOpenPortal}
isOpeningPortal={portal.isPending} isOpeningPortal={portal.isPending}
/> />
)} )}
{/* Toggle mensuel / annuel */} {/* Toggle cycle */}
<div className="self-start"> <div className="flex items-center justify-between gap-4 flex-wrap">
<CycleToggle value={cycle} onChange={setCycle} /> <CycleToggle value={cycle} onChange={setCycle} />
<p className="text-[11.5px] italic text-ink-3">
Annulation 1-clic · pas d'engagement
</p>
</div> </div>
{/* Cards plans */} {/* Layout asymétrique : Pro en hero centré, Free et Business plus modestes */}
<section className="grid grid-cols-1 gap-4 lg:grid-cols-3 lg:gap-5"> <section className="grid grid-cols-1 gap-4 lg:grid-cols-[1fr_1.3fr_1fr] lg:gap-5 lg:items-stretch">
<PlanCard <PlanFree current={currentPlan === "free"} />
plan="free" <PlanProHero
currentPlan={sub?.plan} current={currentPlan === "pro"}
cycle={cycle} cycle={cycle}
ctaDisabled
ctaLabel={sub?.plan === "free" ? "Plan actuel" : "Plan gratuit"}
/>
<PlanCard
plan="pro"
currentPlan={sub?.plan}
cycle={cycle}
highlight
loading={checkout.isPending && checkout.variables?.plan === "pro"} loading={checkout.isPending && checkout.variables?.plan === "pro"}
onUpgrade={() => onUpgrade("pro")} onUpgrade={() => onUpgrade("pro")}
/> />
<PlanCard <PlanBusiness
plan="business" current={currentPlan === "business"}
currentPlan={sub?.plan}
cycle={cycle} cycle={cycle}
loading={checkout.isPending && checkout.variables?.plan === "business"} loading={checkout.isPending && checkout.variables?.plan === "business"}
onUpgrade={() => onUpgrade("business")} onUpgrade={() => onUpgrade("business")}
/> />
</section> </section>
{/* Footer rassurant */}
<p className="text-[12px] text-ink-3 italic max-w-2xl leading-relaxed"> <p className="text-[12px] text-ink-3 italic max-w-2xl leading-relaxed">
Paiement sécurisé via Stripe (CB, SEPA). TVA selon votre pays. Paiement sécurisé via Stripe CB ou SEPA, TVA selon votre pays.
Annulation possible à tout moment depuis le portail client. Aucun Vous gardez la main sur votre abonnement à tout moment depuis le
engagement. portail client.
</p> </p>
</div> </div>
); );
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Card plan courant // Strip "Plan courant" — discret mais informatif (vs gros bloc IA-template)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function CurrentPlanCard({ function CurrentPlanStrip({
state, state,
onOpenPortal, onOpenPortal,
isOpeningPortal, isOpeningPortal,
@ -158,72 +142,71 @@ function CurrentPlanCard({
isOpeningPortal: boolean; isOpeningPortal: boolean;
}) { }) {
const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state; const { plan, activeInvoicesCount, caps, inGracePeriod, gracePeriodEndsAt } = state;
const isLimited = plan === "free" && caps.activeInvoicesLimit !== null;
const limit = caps.activeInvoicesLimit; const limit = caps.activeInvoicesLimit;
const isLimited = plan === "free" && limit !== null;
const limitReached = const limitReached =
isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit; isLimited && limit !== null && !inGracePeriod && activeInvoicesCount >= limit;
return ( return (
<Card padding="md" className="flex flex-col gap-4"> <div
<div className="flex items-start justify-between gap-4 flex-wrap"> className={cn(
<div> "flex items-center gap-4 rounded-card border bg-white px-5 py-4",
<Eyebrow tone="ink">Plan actuel</Eyebrow> "border-line",
<p className="mt-2 font-display text-[22px] font-bold text-ink"> )}
Rubis {planLabel(plan)} >
{state.subscriptionStatus && state.subscriptionStatus !== "active" && ( <Gem size={22} className="shrink-0" />
<span className="ml-2 text-[12px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
· {state.subscriptionStatus} <div className="flex-1 min-w-0">
</span> <p className="font-display text-[15px] font-bold text-ink leading-tight">
)} {plan === "free" ? "Plan Rubis" : "Rubis"}
</p> {plan !== "free" && (
{state.currentPeriodEnd && ( <span className="ml-1 text-rubis">{plan === "pro" ? " Pro" : " Business"}</span>
<p className="mt-1 text-[12.5px] text-ink-3"> )}
Prochaine facture le{" "} {state.subscriptionStatus && state.subscriptionStatus !== "active" && (
<strong className="font-medium text-ink-2"> <span className="ml-2 text-[10.5px] font-medium text-rubis-deep uppercase tracking-[0.1em]">
· {state.subscriptionStatus}
</span>
)}
</p>
<p className="mt-0.5 text-[12px] text-ink-3 leading-tight">
{plan === "free" && inGracePeriod && gracePeriodEndsAt && (
<>
Période illimitée jusqu'au{" "}
<strong className="text-ink-2 font-medium">
{formatDate(gracePeriodEndsAt)}
</strong>
</>
)}
{plan === "free" && !inGracePeriod && limit !== null && (
<>
<strong
className={cn(
"tabular-nums font-medium",
limitReached ? "text-rubis-deep" : "text-ink-2",
)}
>
{activeInvoicesCount} / {limit}
</strong>{" "}
factures actives
{limitReached && " — limite atteinte"}
</>
)}
{plan !== "free" && state.currentPeriodEnd && (
<>
Renouvellement le{" "}
<strong className="text-ink-2 font-medium">
{formatDate(state.currentPeriodEnd)} {formatDate(state.currentPeriodEnd)}
</strong> </strong>
{state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"} {state.billingCycle === "yearly" ? " (annuel)" : " (mensuel)"}
</p> </>
)} )}
{plan === "free" && inGracePeriod && gracePeriodEndsAt && ( </p>
<p className="mt-1 text-[12.5px] text-rubis-deep">
<Sparkles size={12} className="inline mr-1" aria-hidden="true" />
Période de grâce illimité jusqu'au{" "}
<strong className="font-semibold">
{formatDate(gracePeriodEndsAt)}
</strong>
</p>
)}
</div>
{state.hasStripeCustomer && (
<Button
size="sm"
variant="secondary"
onClick={onOpenPortal}
loading={isOpeningPortal}
>
<CreditCard size={14} aria-hidden="true" /> Gérer
</Button>
)}
</div> </div>
{/* Compteur de factures actives — visible pour Free uniquement */} {/* Mini progress bar mobile-OK pour Free */}
{isLimited && limit !== null && ( {isLimited && limit !== null && !inGracePeriod && (
<div> <div className="hidden sm:block w-32 shrink-0">
<div className="flex items-baseline justify-between mb-1"> <div className="h-1 w-full rounded-full bg-cream-2 overflow-hidden">
<p className="text-[12.5px] text-ink-3">
Factures actives en relance
</p>
<p
className={cn(
"text-[12.5px] font-semibold tabular-nums",
limitReached ? "text-rubis-deep" : "text-ink-2",
)}
>
{activeInvoicesCount} / {limit}
</p>
</div>
<div className="h-1.5 w-full rounded-full bg-cream-2 overflow-hidden">
<div <div
className={cn( className={cn(
"h-full transition-[width] duration-300", "h-full transition-[width] duration-300",
@ -234,19 +217,25 @@ function CurrentPlanCard({
}} }}
/> />
</div> </div>
{limitReached && (
<p className="mt-2 text-[12.5px] text-rubis-deep font-medium">
Limite atteinte passez Pro pour ajouter de nouvelles factures.
</p>
)}
</div> </div>
)} )}
</Card>
{state.hasStripeCustomer && (
<Button
size="sm"
variant="ghost"
onClick={onOpenPortal}
loading={isOpeningPortal}
>
<CreditCard size={13} aria-hidden="true" /> Gérer
</Button>
)}
</div>
); );
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Toggle cycle // Toggle cycle — mensuel par défaut, annuel à droite avec 17%
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function CycleToggle({ function CycleToggle({
@ -295,148 +284,221 @@ function CycleToggle({
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Cards plans (Free / Pro / Business) // PlanFree — petite card sobre. Pas de pricing affiché en gros (= 0 €).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const PRICES: Record< function PlanFree({ current }: { current: boolean }) {
PlanKey,
{ monthly: number | null; yearly: number | null }
> = {
free: { monthly: 0, yearly: 0 },
pro: { monthly: 19, yearly: 190 },
business: { monthly: 49, yearly: 490 },
};
const FEATURES_BY_PLAN: Record<PlanKey, string[]> = {
free: [
"5 factures actives en relance",
"1 utilisateur",
"Plans de relance fournis",
"OCR illimité (3 premiers mois)",
],
pro: [
"Factures illimitées",
"OCR illimité",
"Plans custom + IA générative",
"Toutes les automatisations",
"1 utilisateur",
],
business: [
"Tout le plan Pro",
"Réponses depuis votre email pro",
"5 sièges utilisateurs",
"Support prioritaire",
"SMS (à venir)",
],
};
function PlanCard({
plan,
currentPlan,
cycle,
highlight = false,
ctaDisabled = false,
ctaLabel,
loading = false,
onUpgrade,
}: {
plan: PlanKey;
currentPlan?: PlanKey;
cycle: BillingCycle;
highlight?: boolean;
ctaDisabled?: boolean;
ctaLabel?: string;
loading?: boolean;
onUpgrade?: () => void;
}) {
const isCurrent = currentPlan === plan;
const price = PRICES[plan][cycle];
const features = FEATURES_BY_PLAN[plan];
const planIcon =
plan === "pro" ? <Zap size={16} /> : plan === "business" ? <Users size={16} /> : null;
return ( return (
<Card <div className="rounded-card border border-line bg-cream-2/40 p-6 flex flex-col">
padding="md" <div className="flex items-baseline gap-2 mb-1">
className={cn( <p className="font-display text-[16px] font-bold text-ink">Découverte</p>
"flex flex-col", {current && <CurrentBadge />}
highlight && !isCurrent && "border-rubis shadow-card",
)}
>
<div className="flex items-center gap-2 mb-1">
{planIcon && <span className="text-rubis">{planIcon}</span>}
<p className="font-display text-[18px] font-bold text-ink">
{planLabel(plan)}
</p>
{highlight && !isCurrent && (
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
Recommandé
</span>
)}
{isCurrent && (
<span className="ml-auto text-[10px] uppercase tracking-[0.12em] font-semibold text-ink-3">
Actuel
</span>
)}
</div> </div>
<p className="text-[11.5px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-5">
Gratuit
</p>
<div className="mt-2 mb-5"> <p className="font-display text-[28px] font-bold leading-none text-ink mb-1">
<p className="font-display tabular-nums"> 0
<span className="text-[36px] font-bold tracking-[-0.02em] text-ink"> </p>
{price === 0 ? "0" : price} <p className="text-[11.5px] text-ink-3 mb-5">3 mois illimités, puis 5 factures actives</p>
</span>
<span className="text-[14px] font-medium text-ink-3 ml-1">
{cycle === "monthly" ? "€/mois" : "€/an"}
</span>
</p>
{price !== null && price > 0 && cycle === "yearly" && (
<p className="text-[11.5px] text-ink-3">
soit {(price / 12).toFixed(2).replace(".", ",")} /mois
</p>
)}
{plan === "free" && (
<p className="text-[11.5px] text-ink-3">3 mois illimités, puis 5 factures actives</p>
)}
</div>
<ul className="flex flex-col gap-2 mb-6 flex-1"> <ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
{features.map((f) => ( <FeatureLine>Tester sans poser sa CB</FeatureLine>
<li <FeatureLine>Plans de relance fournis</FeatureLine>
key={f} <FeatureLine>OCR + automatisations 3 mois</FeatureLine>
className="flex items-start gap-2 text-[13px] text-ink-2 leading-snug" <FeatureLine>Au-delà : 5 factures à la fois</FeatureLine>
>
<Check size={13} className="text-rubis shrink-0 mt-0.5" aria-hidden="true" />
<span>{f}</span>
</li>
))}
</ul> </ul>
{!ctaDisabled && onUpgrade ? ( <p className="mt-auto pt-6 text-[11px] italic text-ink-3 leading-snug">
<Button Tout ce qu'il faut pour comprendre l'outil. Vous prendrez Pro
size="md" quand votre volume mensuel dépasse 5 factures à relancer.
variant={highlight ? "primary" : "secondary"} </p>
loading={loading} </div>
disabled={isCurrent} );
onClick={onUpgrade} }
className="w-full"
> // ---------------------------------------------------------------------------
{isCurrent ? ( // PlanProHero — la pièce maîtresse. Bg cream + gem en filigrane, prix XL.
"Plan actuel" // ---------------------------------------------------------------------------
) : (
<> function PlanProHero({
Passer {planLabel(plan)} <ArrowRight size={14} aria-hidden="true" /> current,
</> cycle,
)} loading,
</Button> onUpgrade,
) : ( }: {
<Button size="md" variant="ghost" disabled className="w-full"> current: boolean;
{ctaLabel ?? "—"} cycle: BillingCycle;
</Button> loading: boolean;
)} onUpgrade: () => void;
}) {
const monthlyPrice = cycle === "monthly" ? 19 : Math.round((190 / 12) * 100) / 100;
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 190 € facturés annuellement";
return (
<Card padding="none" className="relative overflow-hidden flex flex-col p-7 border-rubis shadow-card">
{/* Gem en filigrane, bas-droite, très large pour devenir un motif */}
<Gem
aria-hidden="true"
size={280}
className="absolute -bottom-16 -right-16 text-rubis-glow opacity-70 pointer-events-none"
/>
{/* Pastille "Recommandé" */}
<div className="relative flex items-center gap-2 mb-3">
<span className="inline-flex items-center gap-1.5 rounded-full bg-rubis text-white px-2.5 py-0.5 text-[10.5px] font-bold uppercase tracking-[0.12em]">
<Gem size={10} className="text-white" />
Le plus pris
</span>
{current && <CurrentBadge />}
</div>
<p className="relative font-display text-[24px] font-bold tracking-[-0.018em] text-ink leading-tight">
Pour les TPE qui n'ont plus jamais à y penser
</p>
{/* Bloc prix — affiché de manière narrative */}
<div className="relative mt-5 mb-5">
<p className="font-display flex items-baseline">
<span className="text-[44px] font-bold tabular-nums tracking-[-0.025em] text-ink leading-none">
{monthlyPrice.toString().replace(".", ",")}
</span>
<span className="ml-2 text-[13px] font-medium text-ink-3">/ mois</span>
</p>
<p className="mt-1 text-[12px] text-ink-3">{totalLabel}</p>
</div>
{/* Liste de bénéfices, formulés en valeur (pas en feature spec) */}
<ul className="relative flex flex-col gap-2.5 text-[13px] text-ink-2 leading-snug mb-7">
<FeatureLine strong>Factures et relances illimitées</FeatureLine>
<FeatureLine>OCR sans limite de volume</FeatureLine>
<FeatureLine>Plans custom + génération IA des relances</FeatureLine>
<FeatureLine>Mode démo pour montrer l'outil aux prospects</FeatureLine>
<FeatureLine>Toutes les automatisations</FeatureLine>
</ul>
<Button
size="md"
variant="primary"
loading={loading}
disabled={current}
onClick={onUpgrade}
className="relative w-full"
>
{current ? (
"Plan actuel"
) : (
<>
Passer Pro <ArrowRight size={14} aria-hidden="true" />
</>
)}
</Button>
<p className="relative mt-3 text-[11px] italic text-ink-3 text-center leading-snug">
Moins cher qu'une heure de votre temps mensuel.
</p>
</Card> </Card>
); );
} }
function planLabel(plan: PlanKey): string { // ---------------------------------------------------------------------------
return plan === "free" ? "Free" : plan === "pro" ? "Pro" : "Business"; // PlanBusiness — card sobre comme Free mais avec accents rubis-deep.
// ---------------------------------------------------------------------------
function PlanBusiness({
current,
cycle,
loading,
onUpgrade,
}: {
current: boolean;
cycle: BillingCycle;
loading: boolean;
onUpgrade: () => void;
}) {
const monthlyPrice = cycle === "monthly" ? 49 : Math.round((490 / 12) * 100) / 100;
const totalLabel = cycle === "monthly" ? "facturé chaque mois" : "soit 490 € facturés annuellement";
return (
<div className="rounded-card border border-line bg-white p-6 flex flex-col">
<div className="flex items-baseline gap-2 mb-1">
<p className="font-display text-[16px] font-bold text-ink">Équipe</p>
{current && <CurrentBadge />}
</div>
<p className="text-[11.5px] uppercase tracking-[0.12em] text-rubis-deep font-semibold mb-5">
Business
</p>
<p className="font-display flex items-baseline mb-1">
<span className="text-[28px] font-bold tabular-nums tracking-[-0.02em] text-ink leading-none">
{monthlyPrice.toString().replace(".", ",")}
</span>
<span className="ml-1.5 text-[12px] font-medium text-ink-3">/ mois</span>
</p>
<p className="text-[11.5px] text-ink-3 mb-5">{totalLabel}</p>
<ul className="flex flex-col gap-2.5 text-[12.5px] text-ink-2 leading-snug">
<FeatureLine strong>Tout Pro, plus :</FeatureLine>
<FeatureLine>5 sièges utilisateurs</FeatureLine>
<FeatureLine>Réponses depuis votre email pro</FeatureLine>
<FeatureLine>Support prioritaire</FeatureLine>
<FeatureLine subtle>SMS (à venir)</FeatureLine>
</ul>
<Button
size="sm"
variant="secondary"
loading={loading}
disabled={current}
onClick={onUpgrade}
className="mt-auto pt-6 w-full"
>
{current ? "Plan actuel" : "Passer Business"}
</Button>
</div>
);
}
// ---------------------------------------------------------------------------
// Atomes
// ---------------------------------------------------------------------------
function FeatureLine({
children,
strong = false,
subtle = false,
}: {
children: React.ReactNode;
strong?: boolean;
subtle?: boolean;
}) {
return (
<li className="flex items-start gap-2">
{/* Petit losange rubis = bullet maison cohérent avec la marque */}
<span
aria-hidden="true"
className={cn(
"mt-1.5 size-[5px] rotate-45 shrink-0",
subtle ? "bg-ink-3/40" : "bg-rubis",
)}
/>
<span
className={cn(
strong && "font-semibold text-ink",
subtle && "text-ink-3 italic",
)}
>
{children}
</span>
</li>
);
}
function CurrentBadge() {
return (
<span className="text-[10px] uppercase tracking-[0.14em] font-semibold text-rubis bg-rubis-glow rounded-full px-2 py-0.5">
Actuel
</span>
);
} }