Referencia / Sub-Skill

components

UBICACIÓN: skills/supafast-ui/references/components.md

supafast-ui · sub-skill: COMPONENTES

Componentes en src/shared/components/ui/. Headless = @base-ui/react (NO Radix). Variantes = CVA. Composición de clases = cn() (clsx + tailwind-merge).

// src/shared/lib/utils.ts
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }

Idiom base-ui: useRender + mergeProps (en lugar de asChild)

Así se hace un componente polimórfico (el equivalente a asChild de Radix):

import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/shared/lib/utils"

const buttonVariants = cva("…clases base…", {
  variants: { variant: {…}, size: {…} },
  defaultVariants: { variant: "default", size: "default" },
})

function Button({ className, variant, size, render, ...props }:
  useRender.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
  return useRender({
    defaultTagName: "button",
    props: mergeProps<"button">(
      { className: cn(buttonVariants({ variant, size, className })) },
      props,
    ),
    render,
    state: { slot: "button", variant, size },
  })
}
export { Button, buttonVariants }

Para renderizar como otra cosa (p.ej. un <Link>): se pasa render={<NavLink to=… />}.

Estados de base-ui: data-checked / data-unchecked

Las primitivas con estado (switch, toggle, checkbox…) exponen data-checked / data-unchecked, no data-[state=checked]. Ejemplo real del Switch:

import { Switch as SwitchPrimitive } from "@base-ui/react/switch"

<SwitchPrimitive.Root
  data-slot="switch"
  className={cn("inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full p-0.5 shadow-xs transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50 data-checked:bg-primary data-unchecked:bg-input", className)}>
  <SwitchPrimitive.Thumb
    data-slot="switch-thumb"
    className="pointer-events-none block size-4 rounded-full bg-background shadow-sm transition-transform data-unchecked:translate-x-0 data-checked:translate-x-4" />
</SwitchPrimitive.Root>

Recetas de clases (copia, no improvises)

Anatomía base de un control: inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium outline-none transition-all disabled:pointer-events-none disabled:opacity-50

Focus ring (universal): focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50

Estado inválido: aria-invalid:border-destructive aria-invalid:ring-destructive/20

Iconos lucide dentro de controles: [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4

Variantes de Button (referencia)

  • defaultbg-primary text-primary-foreground hover:bg-primary/90 border border-black
  • destructivebg-destructive text-white hover:bg-destructive/90
  • outlineborder border-border bg-background hover:bg-accent hover:text-accent-foreground
  • secondarybg-secondary text-secondary-foreground hover:bg-secondary/80
  • ghosthover:bg-accent hover:text-accent-foreground
  • linktext-primary underline-offset-4 hover:underline
  • Tamaños: xs / sm / default / lg / icon / icon-sm / icon-lg (ej. icon = size-9, default = px-5 py-2).

Variantes de Badge (referencia)

Pill rounded-4xl, h-5 text-xs font-medium, iconos size-3. Variante destructive usa fondo tenue: bg-destructive/10 text-destructive (no fondo sólido). Tonos suaves para estados.

Reglas

  • Componente nuevo → clona este idiom (useRender/mergeProps/cva). Nunca metas Radix.
  • Variante nueva → extiende el cva existente, no añadas clases sueltas en el call site.
  • data-slot="…" en cada raíz para poder targetear desde fuera.
  • Iconos: lucide-react, size-4 por defecto.