jengacore.shadcn registry · Tailwind v4 · Radix and Base UI
Components
Product components · @jengacore/case-timeline

CaseTimeline

Disputes and Mulisa investigations: who acted, what, the reference and when; done, current and future steps.

DisputesMulisa
  1. Horn Commercial Bank02 Oct 11:40completed
    Raised a dispute: goods not received
    tx_01J9…H3V
  2. Hub operations02 Oct 12:05completed
    Accepted the case and notified Sahan Mobile Money
  3. Sahan Mobile Moneydue 04 Oct 12:05current step
    Evidence requested: merchant delivery record
  4. Hub operationsdue 06 Oct 12:05upcoming
    Decision

Install

terminal
npx shadcn@latest add @jengacore/case-timeline

Without the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/case-timeline.json

Registry dependencies

npm packages

lucide-react

Source

components/jc/case-timeline.tsx
import * as React from "react"
import { Check } from "lucide-react"

import { IdChip } from "@/components/jc/id-chip"
import { cn } from "@/lib/utils"

export type TimelineStep = {
  id: string
  /** Who acted: "Horn Commercial Bank", "Hub operations", "Aline Uwase". */
  actor: string
  /** What happened, as a sentence: "Raised a dispute: goods not received". */
  action: React.ReactNode
  /** Related reference, shown as an IdChip. */
  reference?: string
  /** Absolute time, already formatted ("02 Oct 11:40"). */
  at: string
  /** Deadline instead of an event: rendered "due 04 Oct 11:40". */
  due?: boolean
  state: "done" | "current" | "future"
}

/**
 * The spine of Disputes (and Mulisa investigations): who acted, what, the reference, and when.
 * Done steps filled `primary`, the current step outlined, future steps hairline.
 * Messages between participants are steps, not chat bubbles.
 */
export function CaseTimeline({ steps, className }: { steps: TimelineStep[]; className?: string }) {
  return (
    <ol data-slot="case-timeline" className={cn("relative grid gap-0", className)}>
      {steps.map((s, i) => (
        <li key={s.id} className="relative grid grid-cols-[20px_1fr] gap-x-3 pb-5 last:pb-0">
          {i < steps.length - 1 ? (
            <span className={cn("absolute top-5 bottom-0 left-[9.5px] w-px", s.state === "done" ? "bg-primary" : "bg-border")} aria-hidden />
          ) : null}
          <span
            className={cn(
              "relative z-10 mt-0.5 grid size-5 place-items-center rounded-full border",
              s.state === "done" && "border-primary bg-primary text-primary-foreground",
              s.state === "current" && "border-2 border-primary bg-card",
              s.state === "future" && "border-border bg-card contrast:border-input"
            )}
            aria-hidden
          >
            {s.state === "done" ? <Check className="size-3 stroke-[2.5]" /> : null}
          </span>
          <div className="min-w-0">
            <div className="flex flex-wrap items-baseline gap-x-2">
              <span className="text-[13.5px] font-medium">{s.actor}</span>
              <span className="font-mono text-xs text-muted-foreground">
                {s.due ? "due " : ""}
                {s.at}
              </span>
              <span className="sr-only">{s.state === "done" ? "completed" : s.state === "current" ? "current step" : "upcoming"}</span>
            </div>
            <div className={cn("mt-0.5 text-[13.5px]", s.state === "future" && "text-muted-foreground")}>{s.action}</div>
            {s.reference ? <IdChip id={s.reference} short className="mt-1.5" /> : null}
          </div>
        </li>
      ))}
    </ol>
  )
}