registry:block
Task Rows
Live agent task status rows — running, failed, completed.
Rendered example
Matched tax and contact IDs12/12
Flagged stale records0
Reading POS export3 files
Scoring stockout risk68%
Cone supplier follow-updraft
Pistachio reorder notedraft
Install
orion-themeuse-staged-sequencespinner-ringstatus-badge
Open registry JSON registry/ui/task-rows.tsx
"use client"
import * as React from "react"
import { useStagedSequence } from "@/registry/hooks/use-staged-sequence"
import { SpinnerRing } from "@/registry/ui/spinner-ring"
import { StatusBadge as Badge } from "@/registry/ui/status-badge"
/**
* TASK ROWS
* Live agent task status — running, failed, completed. The status run
* completes once; task details stay clickable.
*/
const TICKS = [600, 900, 2400, 1400, 2400, 600]
const X_ICON = (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
)
const CHECK_ICON = (
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M20 6L9 17l-5-5" />
</svg>
)
const RETRY_ICON = (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6" />
</svg>
)
export function TaskRows({ variant = "Capsules" }: { variant?: string }) {
const tick = useStagedSequence(TICKS)
const [manualOpen, setManualOpen] = React.useState<Record<string, boolean>>({})
const row2: "pending" | "failed" | "done" = tick < 3 ? "pending" : tick === 3 ? "failed" : "done"
const rows = [
{
key: "verify",
badge: <Badge tone="green">{CHECK_ICON}</Badge>,
label: "Verified vendor records",
amount: "12 suppliers",
pill: (
<span className="inline-flex h-5.5 items-center rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green">
Completed
</span>
),
details: [
{ label: "Matched tax and contact IDs", meta: "12/12" },
{ label: "Flagged stale records", meta: "0" },
],
},
{
key: "index",
badge: <SpinnerRing active>2</SpinnerRing>,
label: "Build reorder task list",
amount: "7 SKUs",
pill: null,
details: [
{ label: "Reading POS export", meta: "3 files" },
{ label: "Scoring stockout risk", meta: "68%" },
],
},
{
key: "draft",
badge:
row2 === "pending" ? (
<SpinnerRing>3</SpinnerRing>
) : row2 === "failed" ? (
<Badge tone="red">{X_ICON}</Badge>
) : (
<Badge tone="green">{CHECK_ICON}</Badge>
),
label: "Draft supplier emails",
amount: "2 messages",
pill:
row2 === "failed" ? (
<span
className="inline-flex h-5.5 items-center gap-1.5 rounded-full bg-red-tint px-2 text-[11.5px] font-medium text-red"
style={{ animation: "fade-in 200ms ease-out both" }}
>
Failed{" "}
<span style={{ animation: "spin 1.2s linear infinite" }} className="flex">
{RETRY_ICON}
</span>
</span>
) : row2 === "done" ? (
<span
className="inline-flex h-5.5 items-center gap-1.5 rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green"
style={{ animation: "fade-in 200ms ease-out both" }}
>
Completed
</span>
) : null,
details: [
{ label: "Cone supplier follow-up", meta: "draft" },
{ label: "Pistachio reorder note", meta: "draft" },
],
},
]
const list = variant === "List"
return (
<div
className={`flex w-full max-w-110 flex-col ${
list ? "gap-0 self-start overflow-hidden rounded-card bg-surface shadow-card" : "min-h-[196px] gap-2"
}`}
>
{rows.map((row, i) => {
const open = manualOpen[row.key] ?? (row.key === "index" && tick === 2)
return (
<div
key={row.key}
className={`self-stretch overflow-hidden transition-[border-radius] duration-300 ${
list ? "border-b border-line last:border-0" : "bg-surface shadow-card"
}`}
style={{
borderRadius: list ? 0 : open ? 14 : 22,
animation: `fade-up 450ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both`,
}}
>
<button
type="button"
aria-expanded={open}
onClick={() => setManualOpen((current) => ({ ...current, [row.key]: !open }))}
className="flex h-11 w-full items-center gap-2.5 px-2.5 text-left transition-colors duration-100 hover:bg-inset"
>
<span className="flex size-6 shrink-0 items-center justify-center">{row.badge}</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-ink">{row.label}</span>
<span className="text-[12.5px] text-ink-2 tabular-nums">{row.amount}</span>
{row.pill}
<span
aria-hidden="true"
className="-ml-2 flex size-7 shrink-0 items-center justify-center rounded-full text-ink-3"
>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-transform duration-300"
style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
>
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</button>
<div
className="grid transition-[grid-template-rows,opacity] duration-300"
style={{
gridTemplateRows: open ? "1fr" : "0fr",
opacity: open ? 1 : 0,
transitionTimingFunction: "cubic-bezier(0.23, 1, 0.32, 1)",
}}
>
<div className="overflow-hidden">
<div className="mb-2.5 grid grid-cols-[24px_1fr] gap-2.5 px-2.5">
<span aria-hidden className="mx-auto h-full w-px bg-line" />
<div className="flex flex-col gap-1.5">
{row.details.map((d, j) => (
<div
key={d.label}
className="flex items-center justify-between"
style={
open
? { animation: `fade-up 300ms cubic-bezier(0.23,1,0.32,1) ${120 + j * 100}ms both` }
: undefined
}
>
<span className="text-[12px] text-ink-2">{d.label}</span>
<span className="font-mono text-[11.5px] text-ink-3 tabular-nums">{d.meta}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
)
}