Saltar al contenido
ssntl/ui

List

Filas separadas por líneas finas alineadas al texto, con un valor final en dígitos tabulares.

Top customers

  • Ana López

    Studio Norte

    Q 4,820.00

  • Marco Ruiz

    Café Lumen

    Q 1,265.50

  • Sofía García

    sofia@ferreteria.gt

    Q 312.00

  • Diego Castillo

    Casa Barrios

    Q 98.75

Instalación

$ pnpm dlx shadcn@latest add @ssntl/list

Uso

tsx
import { List, ListItem, ListItemContent, ListItemMedia, ListItemMeta, ListItemTitle, ListItemValue } from "@/components/ui/list"

<List>
  <ListItem>
    <ListItemMedia aria-hidden>AL</ListItemMedia>
    <ListItemContent>
      <ListItemTitle>Ana López</ListItemTitle>
      <ListItemMeta>Studio Norte</ListItemMeta>
    </ListItemContent>
    <ListItemValue>Q 4,820.00</ListItemValue>
  </ListItem>
</List>
  • Separadores de sección: una línea de 1px con el token border entre filas, así una lista de filas parecidas se lee como elementos separados.
  • La línea empieza donde empieza el texto (56px con media), y los avatares quedan en una columna limpia.
  • Los valores se alinean a la derecha con dígitos tabulares y la misma precisión.
  • Los nombres largos se truncan en una línea; el valor completo va en la vista de detalle.

Código

components/ui/list.tsx
import * as React from "react"

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

// A list of rows split by hairlines. The divider starts where the text does,
// past the media, so rows read as one block and the faces or icons stay in
// one clean column. Dividers are structure, the one place a border belongs.
function List({ className, ...props }: React.ComponentProps<"ul">) {
  return <ul data-slot="list" className={cn("grid", className)} {...props} />
}

function ListItem({ className, ...props }: React.ComponentProps<"li">) {
  return (
    <li
      data-slot="list-item"
      className={cn(
        "relative flex min-h-16 items-center gap-4 py-3",
        // Every row after the first draws a 1px line on top. With media it
        // starts at 56px (40 media + 16 gap), where the text starts.
        "not-first:before:absolute not-first:before:inset-x-0 not-first:before:top-0 not-first:before:h-px not-first:before:bg-border has-data-[slot=list-item-media]:not-first:before:start-14",
        className
      )}
      {...props}
    />
  )
}

/** A 40px avatar or icon well; decorative unless it carries meaning. */
function ListItemMedia({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="list-item-media"
      className={cn(
        "grid size-10 shrink-0 place-items-center overflow-hidden rounded-full bg-secondary text-caption font-medium text-foreground [&_svg]:size-5",
        className
      )}
      {...props}
    />
  )
}

function ListItemContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="list-item-content"
      className={cn("grid min-w-0 flex-1 gap-0.5", className)}
      {...props}
    />
  )
}

function ListItemTitle({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="list-item-title"
      className={cn("truncate text-label", className)}
      {...props}
    />
  )
}

function ListItemMeta({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="list-item-meta"
      className={cn("truncate text-caption text-muted-foreground", className)}
      {...props}
    />
  )
}

/** A trailing number: right-aligned with tabular figures, so values line up. */
function ListItemValue({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="list-item-value"
      className={cn("shrink-0 text-end text-label tabular-nums", className)}
      {...props}
    />
  )
}

export {
  List,
  ListItem,
  ListItemContent,
  ListItemMedia,
  ListItemMeta,
  ListItemTitle,
  ListItemValue,
}