rubis/apps/web/src/components/ui/Button.tsx
ordinarthur 8d3bab6a89 feat: scaffold frontend monorepo + first /login screen
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>
2026-05-06 10:10:48 +02:00

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 };