Files
inkreach-agent/docs/ui-ref/beautifului/task-rows.json

18 lines
11 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "task-rows",
"type": "registry:component",
"title": "Task Rows",
"description": "Live agent task status — running, failed, completed.",
"registryDependencies": [
"https://www.beautifului.dev/r/foundation.json"
],
"files": [
{
"path": "components/primitives/TaskRows.tsx",
"content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * TASK ROWS\n *\n * 0ms rows enter staggered (80ms apart)\n * 600ms row 1 ring sweeps 0 → 66%\n * 1500ms row 1 expands — detail steps drop down\n * 3900ms row 1 collapses; row 2 flips to Failed + retry\n * 5300ms row 2 resolves to Completed\n * The status run completes once; task details stay clickable.\n * ───────────────────────────────────────────────────────── */\n\nconst TICKS = [600, 900, 2400, 1400, 2400, 600];\n\nfunction useTick(intervals: number[]) {\n const [tick, setTick] = useState(0);\n useEffect(() => {\n if (tick >= intervals.length - 1) return;\n const t = setTimeout(() => setTick((x) => x + 1), intervals[tick]);\n return () => clearTimeout(t);\n }, [tick, intervals]);\n return tick;\n}\n\nfunction SpinnerRing({ active, children }: { active?: boolean; children?: React.ReactNode }) {\n const size = 24, stroke = 2;\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n return (\n <span className=\"relative inline-flex shrink-0 items-center justify-center\" style={{ width: size, height: size }}>\n <svg\n width={size} height={size} className=\"absolute inset-0\"\n style={active ? { animation: \"spin 1.1s linear infinite\" } : undefined}\n >\n <circle cx={size / 2} cy={size / 2} r={r} fill=\"none\" stroke=\"var(--line)\" strokeWidth={stroke} />\n {active && (\n <circle\n cx={size / 2} cy={size / 2} r={r} fill=\"none\"\n stroke=\"var(--ink-3)\" strokeWidth={stroke} strokeLinecap=\"round\"\n strokeDasharray={`${c * 0.28} ${c * 0.72}`}\n />\n )}\n </svg>\n <span className=\"relative text-[10.5px] font-semibold tabular-nums text-ink\">{children}</span>\n </span>\n );\n}\n\nfunction Badge({ tone, children }: { tone: \"red\" | \"green\"; children: React.ReactNode }) {\n return (\n <span\n className={`flex size-5.5 shrink-0 items-center justify-center rounded-full text-white\n ${tone === \"red\" ? \"bg-red\" : \"bg-green\"}`}\n style={{ animation: \"pop-in 300ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n {children}\n </span>\n );\n}\n\nconst XIcon = (\n <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>\n);\nconst CheckIcon = (\n <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>\n);\nconst RetryIcon = (\n <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>\n);\n\n/* One detail line shown when a task row is expanded. */\nexport type TaskDetail = { label: string; meta: string };\n\n/* A single task row.\n * - \"done\" → green check badge + completed pill (static)\n * - \"running\" → active spinner showing `step`, no pill (static)\n * - \"sequence\" → animation-driven: pending spinner → failed → completed\n */\nexport type TaskRow = {\n key: string;\n label: string;\n amount: string;\n status: \"done\" | \"running\" | \"sequence\";\n step?: number;\n details: TaskDetail[];\n};\n\nexport type TaskRowsLabels = {\n completed: string;\n failed: string;\n};\n\nconst DEFAULT_LABELS: TaskRowsLabels = {\n completed: \"Completed\",\n failed: \"Failed\",\n};\n\nconst TASK_ROWS: TaskRow[] = [\n {\n key: \"verify\",\n label: \"Verified vendor records\",\n amount: \"12 suppliers\",\n status: \"done\",\n details: [\n { label: \"Matched tax and contact IDs\", meta: \"12/12\" },\n { label: \"Flagged stale records\", meta: \"0\" },\n ],\n },\n {\n key: \"index\",\n label: \"Build reorder task list\",\n amount: \"7 SKUs\",\n status: \"running\",\n step: 2,\n details: [\n { label: \"Reading POS export\", meta: \"3 files\" },\n { label: \"Scoring stockout risk\", meta: \"68%\" },\n ],\n },\n {\n key: \"draft\",\n label: \"Draft supplier emails\",\n amount: \"2 messages\",\n status: \"sequence\",\n step: 3,\n details: [\n { label: \"Cone supplier follow-up\", meta: \"draft\" },\n { label: \"Pistachio reorder note\", meta: \"draft\" },\n ],\n },\n];\n\nexport default function TaskRows({\n variant = \"Capsules\",\n rows = TASK_ROWS,\n labels,\n className,\n onToggleRow,\n}: {\n variant?: string;\n rows?: TaskRow[];\n labels?: Partial<TaskRowsLabels>;\n className?: string;\n onToggleRow?: (key: string, open: boolean) => void;\n}) {\n const tick = useTick(TICKS);\n const [manualOpen, setManualOpen] = useState<Record<string, boolean>>({});\n const row2: \"pending\" | \"failed\" | \"done\" = tick < 3 ? \"pending\" : tick === 3 ? \"failed\" : \"done\";\n const copy = { ...DEFAULT_LABELS, ...labels };\n\n const badgeFor = (row: TaskRow) => {\n if (row.status === \"done\") return <Badge tone=\"green\">{CheckIcon}</Badge>;\n if (row.status === \"running\") return <SpinnerRing active>{row.step}</SpinnerRing>;\n return row2 === \"pending\" ? (\n <SpinnerRing>{row.step}</SpinnerRing>\n ) : row2 === \"failed\" ? (\n <Badge tone=\"red\">{XIcon}</Badge>\n ) : (\n <Badge tone=\"green\">{CheckIcon}</Badge>\n );\n };\n\n const pillFor = (row: TaskRow) => {\n if (row.status === \"done\")\n return (\n <span className=\"inline-flex h-5.5 items-center rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green\">\n {copy.completed}\n </span>\n );\n if (row.status === \"running\") return null;\n return row2 === \"failed\" ? (\n <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\" }}>\n {copy.failed} <span style={{ animation: \"spin 1.2s linear infinite\" }} className=\"flex\">{RetryIcon}</span>\n </span>\n ) : row2 === \"done\" ? (\n <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\" }}>\n {copy.completed}\n </span>\n ) : null;\n };\n\n const list = variant === \"List\";\n return (\n <div\n className={`flex w-full max-w-110 flex-col ${\n list ? \"gap-0 self-start overflow-hidden rounded-card bg-surface shadow-card\" : \"min-h-[196px] gap-2\"\n }${className ? ` ${className}` : \"\"}`}\n >\n {rows.map((row, i) => {\n const open = manualOpen[row.key] ?? (row.key === \"index\" && tick === 2);\n return (\n <div\n key={row.key}\n className={`self-stretch overflow-hidden transition-[border-radius,background-color] duration-300 hover:bg-inset ${\n list ? \"border-b border-line last:border-0\" : \"bg-surface shadow-card\"\n }`}\n style={{\n borderRadius: list ? 0 : open ? 14 : 22,\n animation: `fade-up 450ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both`,\n }}\n >\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() => {\n setManualOpen((current) => ({ ...current, [row.key]: !open }));\n onToggleRow?.(row.key, !open);\n }}\n className=\"flex h-11 w-full items-center gap-2.5 px-2.5 text-left\"\n >\n <span className=\"flex size-6 shrink-0 items-center justify-center\">\n {badgeFor(row)}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[13px] font-medium text-ink\">\n {row.label}\n </span>\n <span className=\"text-[12.5px] text-ink-2 tabular-nums\">{row.amount}</span>\n {pillFor(row)}\n <span\n aria-hidden=\"true\"\n className=\"-ml-2 flex size-7 shrink-0 items-center justify-center rounded-full text-ink-3\"\n >\n <svg\n width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className=\"transition-transform duration-300\"\n style={{ transform: open ? \"rotate(180deg)\" : \"rotate(0)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </span>\n </button>\n\n {/* dropdown detail — same expandable grammar as Chain of Thought */}\n <div\n className=\"grid transition-[grid-template-rows,opacity] duration-300\"\n style={{\n gridTemplateRows: open ? \"1fr\" : \"0fr\",\n opacity: open ? 1 : 0,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n >\n <div className=\"overflow-hidden\">\n <div className=\"mb-2.5 grid grid-cols-[24px_1fr] gap-2.5 px-2.5\">\n <span aria-hidden className=\"mx-auto h-full w-px bg-line\" />\n <div className=\"flex flex-col gap-1.5\">\n {row.details.map((d, j) => (\n <div\n key={d.label}\n className=\"flex items-center justify-between\"\n style={\n open\n ? { animation: `fade-up 300ms cubic-bezier(0.23,1,0.32,1) ${120 + j * 100}ms both` }\n : undefined\n }\n >\n <span className=\"text-[12px] text-ink-2\">{d.label}</span>\n <span className=\"font-mono text-[11.5px] text-ink-3 tabular-nums\">\n {d.meta}\n </span>\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/primitives/TaskRows.tsx"
}
]
}