Monorepo Turborepo (pnpm workspaces) avec 3 packages :
- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
• TanStack Router (file-based, auto code-splitting), Query, Form
• Radix primitives bruts + CVA + clsx + tailwind-merge
• MSW pour mocker l'API tant qu'Adonis n'est pas branché
• Polices Bricolage Grotesque + Inter self-hostées via fontsource
• Tokens marque (rubis, cream, ink) exposés via @theme
• Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
• Route /login full flow : TanStack Form + Zod + mutation Query
- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
• Auth access tokens (Bearer) — cf. ADR-017
• Tuyau core déjà câblé pour la génération de types
• Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
• Minimal — uniquement le pont front ↔ back
- packages/shared : types TS + schemas Zod + constantes
• Source unique de vérité partagée api ↔ web
• Domaines : User, Org, Auth, Client, Invoice, Plan
Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.
CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.
Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { forwardRef } from "react";
|
|
import { Slot } from "@radix-ui/react-slot";
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Bouton — primitive maison.
|
|
*
|
|
* Personnalité :
|
|
* - Border-radius 6px (sharper que la default Tailwind, cohérent landing)
|
|
* - Shadow rubis-teintée sur primary (pas de shadow plate générique)
|
|
* - Micro-translateY au hover (le bouton "soulève" légèrement)
|
|
* - Pas de focus ring bleu — anneau rubis-glow discret
|
|
* - Variants explicites : primary / secondary / ghost / link / danger
|
|
*
|
|
* Composition via Radix Slot : `<Button asChild><Link to=…>` propage les styles
|
|
* sans wrapper supplémentaire.
|
|
*/
|
|
const buttonVariants = cva(
|
|
cn(
|
|
// Base
|
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-default",
|
|
"font-sans font-semibold transition-[transform,background,box-shadow,color] duration-150",
|
|
// États génériques
|
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
// Focus ring discret rubis-glow — pas de blue ring browser default
|
|
"focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-rubis-glow",
|
|
"focus-visible:ring-offset-0",
|
|
),
|
|
{
|
|
variants: {
|
|
variant: {
|
|
primary: cn(
|
|
"bg-rubis text-white shadow-rubis",
|
|
"hover:bg-rubis-deep hover:-translate-y-px hover:shadow-rubis-hover",
|
|
"active:translate-y-0 active:shadow-rubis",
|
|
),
|
|
secondary: cn(
|
|
"bg-transparent text-ink border border-ink",
|
|
"hover:bg-ink hover:text-cream",
|
|
),
|
|
ghost: cn(
|
|
"bg-transparent text-ink",
|
|
"hover:bg-cream-2",
|
|
),
|
|
link: cn(
|
|
"bg-transparent text-rubis underline-offset-4 hover:underline px-0 py-0 h-auto",
|
|
"shadow-none",
|
|
),
|
|
danger: cn(
|
|
"bg-rubis-deep text-white",
|
|
"hover:bg-rubis-deep/90",
|
|
),
|
|
},
|
|
size: {
|
|
sm: "h-9 px-3 text-[13px]",
|
|
md: "h-11 px-[22px] py-[13px] text-[15px]",
|
|
lg: "h-12 px-7 text-base",
|
|
icon: "size-10 px-0",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "primary",
|
|
size: "md",
|
|
},
|
|
},
|
|
);
|
|
|
|
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
VariantProps<typeof buttonVariants> & {
|
|
/** Si true, rend l'enfant en propageant les styles (cf. Radix Slot). */
|
|
asChild?: boolean;
|
|
/** État chargement : remplace le contenu par un spinner discret. */
|
|
loading?: boolean;
|
|
};
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant, size, asChild = false, loading = false, children, disabled, ...props }, ref) => {
|
|
const Comp = asChild ? Slot : "button";
|
|
return (
|
|
<Comp
|
|
ref={ref}
|
|
className={cn(buttonVariants({ variant, size }), className)}
|
|
disabled={disabled || loading}
|
|
{...props}
|
|
>
|
|
{loading ? <ButtonSpinner /> : children}
|
|
</Comp>
|
|
);
|
|
},
|
|
);
|
|
Button.displayName = "Button";
|
|
|
|
function ButtonSpinner() {
|
|
return (
|
|
<span
|
|
aria-hidden="true"
|
|
className="inline-block size-4 animate-spin rounded-full border-2 border-current border-r-transparent"
|
|
/>
|
|
);
|
|
}
|
|
|
|
export { buttonVariants };
|