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"
}