SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
7.8 KB · 226 lines tsx
Raw Blame History
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