Product components · @jengacore/case-timeline
CaseTimeline
Disputes and Mulisa investigations: who acted, what, the reference and when; done, current and future steps.
DisputesMulisa
- Horn Commercial Bank02 Oct 11:40completedRaised a dispute: goods not receivedtx_01J9…H3V
- Hub operations02 Oct 12:05completedAccepted the case and notified Sahan Mobile Money
- Sahan Mobile Moneydue 04 Oct 12:05current stepEvidence requested: merchant delivery record
- Hub operationsdue 06 Oct 12:05upcomingDecision
Install
terminal
npx shadcn@latest add @jengacore/case-timelineWithout 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>
)
}