{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "deploys-1",
  "title": "Deploys 1: deploy history",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The deploy history of a product: version, what changed, who shipped it and when, how it ended in words, one environment at a time, the live version marked, and a confirmed rollback to any earlier version that succeeded.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/alert-dialog.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/return-focus.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/deploys-1/deploys-1.tsx",
      "content": "// Ballmac UI: Deploys 1. https://ui.ballmac.com/blocks/deploys-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, CircleX, GitCommitHorizontal, Loader2, Rocket, Undo2, type LucideIcon } from \"lucide-react\"\n\nimport { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from \"@/components/ballmac/alert-dialog\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { useReturnFocus } from \"@/lib/ballmac/return-focus\"\nimport { cn } from \"@/lib/utils\"\n\ntype Deploys1Status = \"success\" | \"failed\" | \"rolled-back\" | \"running\"\n\ntype Deploys1Deploy = {\n  id: string\n  /** For example \"v2.18.4\". */\n  version: string\n  /** What changed, in a line. */\n  message: string\n  author: string\n  /** When it happened, already formatted, such as \"Today 14:02\". */\n  time: string\n  environment: string\n  status: Deploys1Status\n  /** How long it took, already formatted. */\n  duration?: string\n  /** A short commit id. */\n  commit?: string\n}\n\ntype Deploys1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    deploys?: Deploys1Deploy[]\n    title?: string\n    /** Called after the confirmation. Reject and nothing changes, with the message. */\n    onRollback?: (to: Deploys1Deploy) => void | Promise<void>\n  }\n\nconst sample: Deploys1Deploy[] = [\n  { id: \"d1\", version: \"v2.18.4\", message: \"Retry the invoice export with backoff\", author: \"Priya Shah\", time: \"Today 14:02\", environment: \"production\", status: \"success\", duration: \"3 min 12 s\", commit: \"a41f9c2\" },\n  { id: \"d2\", version: \"v2.18.3\", message: \"Move the retry into the shared helper\", author: \"Theo Nakamura\", time: \"Today 10:41\", environment: \"production\", status: \"failed\", duration: \"1 min 55 s\", commit: \"c93d1e0\" },\n  { id: \"d3\", version: \"v2.18.2\", message: \"Fix the totals on the billing page\", author: \"Noor Haddad\", time: \"Yesterday 17:20\", environment: \"production\", status: \"success\", duration: \"3 min 04 s\", commit: \"7be20a4\" },\n  { id: \"d4\", version: \"v2.18.1\", message: \"Add the assistant's refund tool\", author: \"Kofi Mensah\", time: \"Yesterday 11:08\", environment: \"production\", status: \"rolled-back\", duration: \"3 min 20 s\", commit: \"e55d018\" },\n  { id: \"d5\", version: \"v2.18.0\", message: \"New onboarding checklist\", author: \"Ana Souza\", time: \"22 Oct 15:30\", environment: \"production\", status: \"success\", duration: \"3 min 31 s\", commit: \"19ac6f3\" },\n  { id: \"d6\", version: \"v2.19.0-rc1\", message: \"Dark mode for the dashboard\", author: \"Oliver Reed\", time: \"Today 09:15\", environment: \"staging\", status: \"running\", commit: \"f08b7d9\" },\n  { id: \"d7\", version: \"v2.18.4\", message: \"Retry the invoice export with backoff\", author: \"Priya Shah\", time: \"Today 13:20\", environment: \"staging\", status: \"success\", duration: \"2 min 58 s\", commit: \"a41f9c2\" },\n]\n\nconst STATUS: Record<Deploys1Status, { icon: LucideIcon; label: string }> = {\n  success: { icon: Check, label: \"Succeeded\" },\n  failed: { icon: CircleX, label: \"Failed\" },\n  \"rolled-back\": { icon: Undo2, label: \"Rolled back\" },\n  running: { icon: Loader2, label: \"Running\" },\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-3\">\n      {[0, 1, 2, 3, 4].map((i) => (\n        <Skeleton key={i} className=\"h-24 rounded-xl\" />\n      ))}\n    </div>\n  )\n}\n\n/**\n * The deploy history of a product: each deploy with its version, what changed, who shipped it, when, and how it ended in words, one environment at a time.\n * The live version is marked, and any earlier one that succeeded can be rolled back to after a confirmation that names the versions involved.\n */\nfunction Deploys1({ deploys: deploysProp = sample, title = \"Deploys\", onRollback, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Deploys1Props) {\n  const id = React.useId()\n  const [deploys, setDeploys] = React.useState(deploysProp)\n  React.useEffect(() => setDeploys(deploysProp), [deploysProp])\n  const envs = [...new Set(deploysProp.map((d) => d.environment))]\n  const [env, setEnv] = React.useState(envs[0] ?? \"production\")\n  const [confirm, setConfirm] = React.useState<Deploys1Deploy | null>(null)\n  const [busy, setBusy] = React.useState(false)\n  const [note, setNote] = React.useState<{ ok: boolean; text: string } | null>(null)\n  const ret = useReturnFocus(!!confirm)\n  const view = state === \"ready\" && deploysProp.length === 0 ? \"empty\" : state\n  const list = deploys.filter((d) => d.environment === env)\n  const live = list.find((d) => d.status === \"success\")\n\n  async function rollback(to: Deploys1Deploy) {\n    setBusy(true)\n    setNote(null)\n    try {\n      await onRollback?.(to)\n      const entry: Deploys1Deploy = { id: `rb-${to.id}-${deploys.length}`, version: to.version, message: `Rolled back to ${to.version}`, author: \"You\", time: \"Just now\", environment: env, status: \"success\", commit: to.commit }\n      setDeploys((l) => [entry, ...l.map((x) => (x.id === live?.id ? { ...x, status: \"rolled-back\" as const } : x))])\n      setNote({ ok: true, text: `${env} is now on ${to.version}.` })\n    } catch (e) {\n      setNote({ ok: false, text: e instanceof Error ? e.message : `The rollback to ${to.version} did not start. ${env} is unchanged.` })\n    } finally {\n      setBusy(false)\n    }\n  }\n\n  return (\n    <section data-slot=\"deploys-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-4xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8 flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n            {title}\n          </h1>\n          <p className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">What went out, who sent it, and how it ended. Go back to an earlier version when you need to.</p>\n        </div>\n        {view === \"ready\" && envs.length > 1 && (\n          <div role=\"group\" aria-label=\"Environment\" className=\"bg-muted/60 inline-flex rounded-full p-0.5\">\n            {envs.map((e) => (\n              <button key={e} type=\"button\" aria-pressed={env === e} onClick={() => { setEnv(e); setNote(null) }} className={cn(\"focus-visible:ring-ring/50 rounded-full px-4 py-1.5 text-sm font-medium capitalize outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", env === e ? \"bg-foreground text-background\" : \"text-muted-foreground hover:text-foreground\")}>\n                {e}\n              </button>\n            ))}\n          </div>\n        )}\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading deploys\" loading={<Skel />} empty={{ title: \"Nothing deployed yet\", description: \"The first deploy to an environment will appear here.\", ...emptyState }} error={{ title: \"Deploys could not be loaded\", description: \"Nothing was rolled back or changed. Check your connection and try again.\", ...errorState }}>\n        <div aria-live=\"polite\">{note && <p role={note.ok ? \"status\" : \"alert\"} className={cn(\"mb-3 text-sm font-medium\", !note.ok && \"text-destructive\")}>{note.text}</p>}</div>\n        <ol aria-label={`Deploys to ${env}, newest first`} className=\"grid gap-0\">\n          {list.map((d, i) => {\n            const S = STATUS[d.status]\n            const isLive = live?.id === d.id\n            const canBack = d.status === \"success\" && !isLive && !!live\n            return (\n              <li key={d.id} data-status={d.status} className=\"relative grid grid-cols-[1.5rem_1fr] gap-4 pb-5 last:pb-0\">\n                <span aria-hidden=\"true\" className=\"relative flex justify-center\">\n                  {i < list.length - 1 && <span className=\"bg-border absolute top-7 bottom-[-1.25rem] w-px\" />}\n                  <span className={cn(\"bg-card z-10 mt-1 flex size-6 items-center justify-center rounded-full border-2\", d.status === \"success\" && \"border-foreground\", d.status === \"failed\" && \"border-destructive text-destructive\", d.status === \"running\" && \"border-chart-1\")}>\n                    <S.icon className={cn(\"size-3\", d.status === \"running\" && \"motion-safe:animate-spin\")} style={d.status === \"running\" ? { animationPlayState: \"var(--bm-ambient-state, running)\" } : undefined} />\n                  </span>\n                </span>\n                <div className={cn(\"bg-card rounded-xl border p-4\", isLive && \"border-foreground\")}>\n                  <div className=\"flex flex-wrap items-start justify-between gap-x-6 gap-y-2\">\n                    <div className=\"min-w-0\">\n                      <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n                        <code className=\"font-mono text-sm font-semibold\" dir=\"ltr\">{d.version}</code>\n                        {isLive && <span className=\"bg-foreground text-background inline-flex items-center gap-1 rounded-full px-2 py-px text-[11px] font-medium\"><Rocket aria-hidden=\"true\" className=\"size-3\" /> Live</span>}\n                        <span className={cn(\"text-xs\", d.status === \"failed\" ? \"text-destructive font-semibold\" : \"text-muted-foreground font-medium\")}>{S.label}{d.duration ? ` · ${d.duration}` : \"\"}</span>\n                      </p>\n                      <p className=\"mt-1 text-sm\">{d.message}</p>\n                      <p className=\"text-muted-foreground mt-1 flex flex-wrap items-center gap-x-3 text-xs\">\n                        <span>{d.author} · {d.time}</span>\n                        {d.commit && <span className=\"flex items-center gap-1 font-mono\" dir=\"ltr\"><GitCommitHorizontal aria-hidden=\"true\" className=\"size-3\" />{d.commit}</span>}\n                      </p>\n                    </div>\n                    {canBack && (\n                      <Button size=\"sm\" variant=\"outline\" onClick={() => setConfirm(d)} aria-label={`Roll back ${env} to ${d.version}`}>\n                        <Undo2 aria-hidden=\"true\" className=\"rtl:-scale-x-100\" /> Roll back to this\n                      </Button>\n                    )}\n                  </div>\n                </div>\n              </li>\n            )\n          })}\n        </ol>\n      </StateView>\n\n      <AlertDialog open={!!confirm} onOpenChange={(o) => !o && setConfirm(null)}>\n        <AlertDialogContent {...ret}>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Roll {env} back to {confirm?.version}?</AlertDialogTitle>\n            <AlertDialogDescription>\n              {env} goes from {live?.version} to {confirm?.version} ({confirm?.message.toLowerCase()}). Changes made since then stop being live until they are deployed again.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction destructive disabled={busy} onClick={() => confirm && void rollback(confirm)}>Roll back</AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </section>\n  )\n}\n\nexport { Deploys1, type Deploys1Deploy, type Deploys1Props, type Deploys1Status }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/deploys-1/deploys-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "deploys",
      "releases",
      "rollback",
      "history",
      "ci",
      "cd",
      "versions",
      "devops"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Deploy history. <Deploys1 deploys={[{ id, version, message, author, time, environment, status: 'success' | 'failed' | 'rolled-back' | 'running', duration?, commit? }]} onRollback={(deploy) => Promise} />. The first environment shows at first; a control switches. The newest successful deploy per environment is Live; only earlier successful ones offer Roll back to this, behind a confirmation naming both versions.",
      "whenToUse": [
        "A release page where people check what is live and go back if needed",
        "An audit of what shipped to each environment"
      ],
      "whenNotToUse": [
        "The stages of one deploy; use pipeline-1",
        "Changes to settings; use audit-1"
      ],
      "composesWith": [
        "pipeline-1",
        "incident-1",
        "audit-1",
        "status-1"
      ],
      "a11y": [
        {
          "keys": "Timeline",
          "action": "An ordered list named 'Deploys to production, newest first'; each status is written ('Succeeded · 3 min 12 s', 'Failed', 'Rolled back') with an icon"
        },
        {
          "keys": "Live",
          "action": "Marked with the word Live"
        },
        {
          "keys": "Environment",
          "action": "Toggle buttons with aria-pressed"
        },
        {
          "keys": "Rollback",
          "action": "A button named 'Roll back production to v2.18.2' that opens an alert dialog and returns focus when closed; a failure is an alert"
        }
      ],
      "customization": [
        "deploys with environments",
        "onRollback; reject and nothing changes",
        "Commit ids and versions stay left to right"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "deploys-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton rows under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "Nothing deployed yet",
        "example": "deploys-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says nothing has been deployed yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "deploys-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was rolled back or changed, with Try again (onRetry). A rollback that does not start says why."
      }
    ],
    "examples": [
      "deploys-1-demo",
      "deploys-1-failing",
      "deploys-1-loading",
      "deploys-1-empty",
      "deploys-1-error"
    ],
    "url": "https://ui.ballmac.com/components/deploys-1"
  },
  "categories": [
    "blocks",
    "deploys"
  ],
  "type": "registry:block"
}