Saltar al contenido
ssntl/ui

Card

Una tarjeta plana separada por tono, no por bordes, con los roles de título, meta y cuerpo del sistema.

Live

Online store

Orders, payments and inventory in one place.

Sales this week

Q 48.2k

+12.4% since last week

Instalación

$ pnpm dlx shadcn@latest add @ssntl/card

Uso

tsx
import { Card, CardBody, CardSpacer, CardTitle, CardWell } from "@/components/ui/card"

<Card tone="ground">
  <CardWell><Store /></CardWell>
  <CardSpacer />
  <CardTitle>Tienda en línea</CardTitle>
  <CardBody>Órdenes, pagos e inventario en un solo lugar.</CardBody>
</Card>
  • El tono separa, nunca los bordes: muted dentro de una superficie blanca, ground sobre la página, una sola tarjeta widget por grid.
  • Los roles tipográficos del sistema: título text-heading, meta text-label, cuerpo text-body.
  • El contenido define la altura; las tarjetas de una fila se estiran a la más alta para alinear sus bordes inferiores.

Código

components/ui/card.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

// Essential Design `es-card`: a flat tile separated by tone, never by borders.
// `radius-lg`, tight padding so the content fills it, content sets the height.
const cardVariants = cva(
  "relative flex min-w-0 flex-col gap-2 rounded-lg text-foreground",
  {
    variants: {
      tone: {
        // Inside a white surface.
        muted: "bg-muted",
        // Directly on the page background.
        ground: "bg-card",
        // The one dark tile of a bento grid, in both themes.
        widget: "bg-black text-neutral-50 shadow-edge",
      },
      size: {
        sm: "rounded-md p-5",
        md: "p-6",
        lg: "gap-3 p-8",
      },
    },
    defaultVariants: { tone: "muted", size: "md" },
  }
)

function Card({
  tone,
  size,
  className,
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof cardVariants>) {
  return (
    <div
      data-slot="card"
      data-tone={tone ?? "muted"}
      className={cn(cardVariants({ tone, size, className }))}
      {...props}
    />
  )
}

/** A 44px round icon well at the top of the card. */
function CardWell({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-well"
      className={cn(
        "grid size-11 shrink-0 place-items-center rounded-full bg-secondary text-foreground [&_svg]:size-5",
        "in-data-[tone=widget]:bg-card in-data-[tone=widget]:text-foreground",
        className
      )}
      {...props}
    />
  )
}

/** Pushes the content to the bottom of the card (at least spacing-4). */
function CardSpacer({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div aria-hidden className={cn("min-h-4 flex-1", className)} {...props} />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
  return (
    <h3
      data-slot="card-title"
      className={cn("text-heading", className)}
      {...props}
    />
  )
}

function CardMeta({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="card-meta"
      className={cn(
        "text-label font-normal text-muted-foreground in-data-[tone=widget]:text-neutral-300",
        className
      )}
      {...props}
    />
  )
}

function CardBody({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="card-body"
      className={cn(
        "text-body text-pretty text-muted-foreground in-data-[tone=widget]:text-neutral-300",
        className
      )}
      {...props}
    />
  )
}

export {
  Card,
  CardBody,
  CardMeta,
  CardSpacer,
  CardTitle,
  CardWell,
  cardVariants,
}