Search

Jump to any page.

Data display

List

Bulleted and numbered lists with square markers; nested items hang off the trace.

  • Constellation reads every channel
  • Problems are traced to a cause
    • Grouped across calls, chats and reviews
    • Ranked by volume and trend
      • Week on week
      • Against the same week last year
  • Telos drafts the fix
  1. Name the approvers for each system
  2. Set the limits Telos acts within
    1. Which systems it may change
    2. How many customers one change may touch
  3. Review the first ten actions together
pnpm dlx shadcn@latest add @paragon/list
Brings incn
registry/paragon/components/list.tsx
"use client"

import * as React from "react"
import { cn } from "cn"

type ListVariant = "bullet" | "number"

const ListContext = React.createContext<{ depth: number; variant: ListVariant }>({ depth: -1, variant: "bullet" })

/**
 * Lists for reading. Top-level items take a square marker, the mark's square
 * end, or a mono numeral. Nested items hang off the trace instead of taking a
 * second marker, so depth reads as one line turning, not as more bullets.
 * Every level indents the same: the rail drops from the parent's first letter
 * and turns into text 1.25rem further in.
 */
function List({
  variant = "bullet",
  className,
  ...props
}: React.ComponentProps<"ul"> & { variant?: ListVariant }) {
  const depth = React.useContext(ListContext).depth + 1
  const context = React.useMemo(() => ({ depth, variant }), [depth, variant])
  // Same props either way; an ordered list only adds attributes.
  const Tag = (variant === "number" ? "ol" : "ul") as "ul"

  return (
    <ListContext.Provider value={context}>
      <Tag
        data-slot="list"
        data-variant={variant}
        className={cn(
          "flex flex-col text-[0.9375rem] leading-6 text-foreground/85 [counter-reset:list]",
          // Nested items space with padding, not gap, so the rail runs unbroken between them.
          depth > 0 ? "ms-5 mt-1" : "gap-1.5",
          className
        )}
        {...props}
      />
    </ListContext.Provider>
  )
}

function ListItem({ className, children, ...props }: React.ComponentProps<"li">) {
  const { depth, variant } = React.useContext(ListContext)

  if (depth > 0) {
    return (
      <li
        data-slot="list-item"
        className={cn(
          "relative pb-1 last:pb-0",
          // The rail from the parent's first letter, stopping at the last turn, and the square turn into the text.
          "before:absolute before:-start-5 before:inset-y-0 before:w-px before:bg-border first:before:-top-1 last:before:bottom-[calc(100%-0.8125rem)]",
          "after:absolute after:-start-5 after:top-3 after:h-px after:w-3.5 after:bg-border",
          className
        )}
        {...props}
      >
        {children}
      </li>
    )
  }

  return (
    <li
      data-slot="list-item"
      className={cn(
        "grid grid-cols-[1.75rem_minmax(0,1fr)] items-baseline",
        variant === "bullet"
          ? "before:size-1.25 before:translate-x-0.5 before:-translate-y-[0.1em] before:bg-foreground/45 rtl:before:-translate-x-0.5"
          : "[counter-increment:list] before:font-mono before:text-[0.75rem] before:text-muted-foreground before:tabular-nums before:content-[counter(list,decimal-leading-zero)]",
        className
      )}
      {...props}
    >
      <div className="min-w-0">{children}</div>
    </li>
  )
}

export { List, ListItem }