TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1"use client";2// Primitives UI d'Immbot AI — style sobre, accessible, thème clair/sombre.3import { forwardRef, type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode, type TextareaHTMLAttributes } from "react";45export function cn(...cls: (string | false | null | undefined)[]): string {6 return cls.filter(Boolean).join(" ");7}89// ---------- Button ----------10type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {11 variant?: "primary" | "secondary" | "ghost" | "danger" | "gold";12 size?: "sm" | "md" | "lg" | "icon";13};14export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(15 { variant = "primary", size = "md", className, ...props },16 ref17) {18 const variants = {19 primary: "bg-brand-600 hover:bg-brand-700 text-white shadow-sm disabled:bg-brand-600/50",20 secondary: "bg-card border border-app hover:bg-surface-2 dark:hover:bg-brand-900/40 text-fg",21 ghost: "hover:bg-surface-2 dark:hover:bg-brand-900/40 text-fg",22 danger: "bg-red-600 hover:bg-red-700 text-white",23 gold: "bg-gold-500 hover:bg-gold-600 text-brand-900 font-semibold shadow-sm",24 };25 const sizes = {26 sm: "h-8 px-3 text-[13px] rounded-md gap-1.5",27 md: "h-9.5 px-4 text-sm rounded-lg gap-2 font-semibold",28 lg: "h-11 px-5 text-[15px] rounded-xl gap-2",29 icon: "h-9 w-9 rounded-lg justify-center",30 };31 return (32 <button33 ref={ref}34 className={cn(35 "inline-flex items-center font-medium transition-colors duration-150 disabled:opacity-60 disabled:cursor-not-allowed select-none",36 variants[variant],37 sizes[size],38 className39 )}40 {...props}41 />42 );43});4445// ---------- Input / Textarea / Label ----------46export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(function Input(47 { className, ...props },48 ref49) {50 return (51 <input52 ref={ref}53 className={cn(54 "w-full h-10 px-3.5 rounded-lg bg-card border border-app text-sm text-fg placeholder:text-muted",55 "focus:border-brand-400 focus:ring-2 focus:ring-brand-500/25 outline-none transition-shadow",56 className57 )}58 {...props}59 />60 );61});6263export const Textarea = forwardRef<HTMLTextAreaElement, TextareaHTMLAttributes<HTMLTextAreaElement>>(64 function Textarea({ className, ...props }, ref) {65 return (66 <textarea67 ref={ref}68 className={cn(69 "w-full px-3.5 py-2.5 rounded-lg bg-card border border-app text-sm text-fg placeholder:text-muted",70 "focus:border-brand-400 focus:ring-2 focus:ring-brand-500/25 outline-none transition-shadow resize-none",71 className72 )}73 {...props}74 />75 );76 }77);7879export function Label({ children, htmlFor, className }: { children: ReactNode; htmlFor?: string; className?: string }) {80 return (81 <label htmlFor={htmlFor} className={cn("block text-[13px] font-medium text-fg mb-1.5", className)}>82 {children}83 </label>84 );85}8687// ---------- Card ----------88export function Card({ children, className }: { children: ReactNode; className?: string }) {89 return <div className={cn("bg-card border border-app rounded-xl shadow-[0_1px_3px_rgb(15_35_60/0.06)]", className)}>{children}</div>;90}9192// ---------- Badge ----------93export function Badge({94 children,95 tone = "neutral",96 className,97}: {98 children: ReactNode;99 tone?: "neutral" | "brand" | "gold" | "green" | "red" | "amber";100 className?: string;101}) {102 const tones = {103 neutral: "bg-surface-2 dark:bg-brand-900/50 text-muted",104 brand: "bg-brand-100 dark:bg-brand-900/60 text-brand-700 dark:text-brand-300",105 gold: "bg-gold-500/15 text-gold-600 dark:text-gold-400",106 green: "bg-emerald-500/12 text-emerald-700 dark:text-emerald-400",107 red: "bg-red-500/12 text-red-700 dark:text-red-400",108 amber: "bg-amber-500/14 text-amber-700 dark:text-amber-400",109 };110 return (111 <span className={cn("inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11.5px] font-medium whitespace-nowrap", tones[tone], className)}>112 {children}113 </span>114 );115}116117// ---------- Skeleton ----------118export function Skeleton({ className }: { className?: string }) {119 return <div className={cn("skeleton", className)} aria-hidden />;120}121122// ---------- EmptyState ----------123export function EmptyState({124 icon,125 title,126 description,127 action,128}: {129 icon?: ReactNode;130 title: string;131 description?: string;132 action?: ReactNode;133}) {134 return (135 <div className="flex flex-col items-center justify-center text-center py-14 px-6 animate-fade-up">136 {icon && <div className="mb-3 text-muted [&>svg]:w-9 [&>svg]:h-9 opacity-70">{icon}</div>}137 <h3 className="font-semibold text-fg">{title}</h3>138 {description && <p className="text-sm text-muted mt-1 max-w-sm">{description}</p>}139 {action && <div className="mt-4">{action}</div>}140 </div>141 );142}143144// ---------- ProgressBar ----------145export function ProgressBar({ value, className, tone = "brand" }: { value: number; className?: string; tone?: "brand" | "gold" | "green" }) {146 const tones = { brand: "bg-brand-500", gold: "bg-gold-500", green: "bg-emerald-500" };147 return (148 <div className={cn("h-2 rounded-full bg-surface-2 dark:bg-brand-900/60 overflow-hidden", className)} role="progressbar" aria-valuenow={Math.round(value * 100)} aria-valuemin={0} aria-valuemax={100}>149 <div className={cn("h-full rounded-full transition-all duration-500", tones[tone])} style={{ width: `${Math.min(100, Math.max(0, value * 100))}%` }} />150 </div>151 );152}153154// ---------- Modal ----------155export function Modal({156 open,157 onClose,158 title,159 children,160 wide,161}: {162 open: boolean;163 onClose: () => void;164 title?: string;165 children: ReactNode;166 wide?: boolean;167}) {168 if (!open) return null;169 return (170 <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-6" role="dialog" aria-modal="true" aria-label={title}>171 <div className="absolute inset-0 bg-black/45 animate-fade-in" onClick={onClose} />172 <div className={cn("relative bg-card border border-app rounded-t-2xl sm:rounded-xl shadow-2xl w-full animate-fade-up max-h-[92dvh] flex flex-col", wide ? "sm:max-w-3xl" : "sm:max-w-lg")}>173 {title && (174 <div className="flex items-center justify-between px-5 py-3.5 border-b border-app shrink-0">175 <h2 className="font-semibold text-fg">{title}</h2>176 <button onClick={onClose} aria-label="Fermer" className="text-muted hover:text-fg p-1 rounded-md">✕</button>177 </div>178 )}179 <div className="p-5 overflow-y-auto">{children}</div>180 </div>181 </div>182 );183}184185// ---------- Spinner ----------186export function Spinner({ className }: { className?: string }) {187 return (188 <svg className={cn("animate-spin h-4 w-4", className)} viewBox="0 0 24 24" fill="none" aria-label="Chargement">189 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />190 <path className="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />191 </svg>192 );193}194195// ---------- Tabs (simple) ----------196export function Tabs({197 tabs,198 active,199 onChange,200 className,201}: {202 tabs: { key: string; label: ReactNode }[];203 active: string;204 onChange: (key: string) => void;205 className?: string;206}) {207 return (208 <div className={cn("flex gap-1 p-1 bg-surface-2 dark:bg-brand-900/40 rounded-xl w-fit max-w-full overflow-x-auto", className)} role="tablist">209 {tabs.map((t) => (210 <button211 key={t.key}212 role="tab"213 aria-selected={active === t.key}214 onClick={() => onChange(t.key)}215 className={cn(216 "px-3.5 py-1.5 rounded-lg text-[13px] font-medium whitespace-nowrap transition-colors",217 active === t.key ? "bg-card text-fg shadow-sm" : "text-muted hover:text-fg"218 )}219 >220 {t.label}221 </button>222 ))}223 </div>224 );225}226