dodil/erp-ui
public/r/timeline.json
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "timeline",
"title": "Timeline",
"description": "What was said and done, newest first, grouped by day.",
"dependencies": [
"@radix-ui/themes"
],
"registryDependencies": [
"@erp-ui/format",
"@erp-ui/status-badge"
],
"files": [
{
"path": "src/components/timeline/Timeline.tsx",
"content": "/**\n * Timeline — what was said and done, newest first, grouped by day. The record of a relationship: calls,\n * emails, meetings, notes, and tasks with their state.\n */\nimport { Badge, Box, Flex, Text } from \"@radix-ui/themes\";\nimport type { ReactNode } from \"react\";\n\nimport { formatDate } from \"../../lib/format\";\nimport { StatusBadge } from \"../status-badge/StatusBadge\";\n\nexport interface TimelineItem {\n id: string;\n kind: string;\n title: string;\n body?: string;\n /** ISO time it happened (or is due). */\n at: string;\n actor?: string;\n status?: string | null;\n}\n\nexport function Timeline({ items, empty }: { items: TimelineItem[]; empty?: ReactNode }) {\n if (!items.length) return <Text color=\"gray\">{empty ?? \"Nothing logged yet.\"}</Text>;\n const sorted = [...items].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));\n const days: [string, TimelineItem[]][] = [];\n for (const it of sorted) {\n const day = it.at.slice(0, 10);\n const last = days[days.length - 1];\n if (last && last[0] === day) last[1].push(it); else days.push([day, [it]]);\n }\n return (\n <Flex direction=\"column\" gap=\"4\" asChild>\n <ol aria-label=\"Timeline\" style={{ listStyle: \"none\", padding: 0, margin: 0 }}>\n {days.map(([day, list]) => (\n <li key={day}>\n <Text as=\"div\" size=\"1\" color=\"gray\" weight=\"medium\" mb=\"2\">{formatDate(day)}</Text>\n <Flex direction=\"column\" gap=\"3\" asChild>\n <ul style={{ listStyle: \"none\", padding: 0, margin: 0 }}>\n {list.map((it) => (\n <li key={it.id}>\n <Flex gap=\"3\" align=\"start\">\n <Badge variant=\"soft\" color=\"gray\" style={{ minWidth: 64, justifyContent: \"center\" }}>\n {it.kind.toLowerCase()}\n </Badge>\n <Box style={{ flex: 1 }}>\n <Flex gap=\"2\" align=\"center\">\n <Text weight=\"medium\">{it.title}</Text>\n {it.status && <StatusBadge status={it.status} />}\n </Flex>\n {it.body && <Text as=\"p\" size=\"2\" style={{ whiteSpace: \"pre-wrap\" }}>{it.body}</Text>}\n {it.actor && <Text as=\"p\" size=\"1\" color=\"gray\">{it.actor}</Text>}\n </Box>\n </Flex>\n </li>\n ))}\n </ul>\n </Flex>\n </li>\n ))}\n </ol>\n </Flex>\n );\n}\n",
"type": "registry:component",
"target": "erp-ui/components/timeline/Timeline.tsx"
}
],
"type": "registry:component"
}