dodil/erp-ui

public/r/confirm-dialog.json

{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-dialog",
  "title": "ConfirmDialog",
  "description": "Confirm an irreversible step; asks for a reason when the API needs one; refusals shown in place.",
  "dependencies": [
    "@radix-ui/themes"
  ],
  "files": [
    {
      "path": "src/components/confirm-dialog/ConfirmDialog.tsx",
      "content": "/**\n * ConfirmDialog — \"are you sure\", for the steps that cannot be taken back or that the next person reads:\n * disqualifying a lead, losing a deal, blocking a partner. When the step needs a reason (the API refuses one\n * without it), the dialog asks for it and will not confirm while it is empty.\n *\n * The dialog stays open while the call runs and shows the API's refusal in place, so a person never loses\n * what they typed to an error.\n */\nimport { AlertDialog, Button, Callout, Flex, Text, TextArea } from \"@radix-ui/themes\";\nimport { useState, type ReactNode } from \"react\";\n\nexport interface ConfirmDialogProps {\n  /** The button that opens the dialog. */\n  trigger: ReactNode;\n  title: string;\n  description?: ReactNode;\n  confirmLabel: string;\n  tone?: \"danger\" | \"primary\";\n  /** Ask for a reason; the label of its field, e.g. \"Why is it lost?\". */\n  reason?: { label: string; placeholder?: string };\n  /** Throw to keep the dialog open with the error shown. */\n  onConfirm: (reason: string) => Promise<unknown> | unknown;\n}\n\nexport function ConfirmDialog({ trigger, title, description, confirmLabel, tone = \"primary\", reason, onConfirm }: ConfirmDialogProps) {\n  const [open, setOpen] = useState(false);\n  const [text, setText] = useState(\"\");\n  const [busy, setBusy] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const blocked = !!reason && text.trim() === \"\";\n\n  const confirm = async () => {\n    setBusy(true);\n    setError(null);\n    try {\n      await onConfirm(text.trim());\n      setOpen(false);\n      setText(\"\");\n    } catch (e) {\n      setError(e instanceof Error ? e.message : String(e));\n    } finally {\n      setBusy(false);\n    }\n  };\n\n  return (\n    <AlertDialog.Root open={open} onOpenChange={(o) => { setOpen(o); if (!o) setError(null); }}>\n      <AlertDialog.Trigger>{trigger}</AlertDialog.Trigger>\n      <AlertDialog.Content maxWidth=\"460px\">\n        <AlertDialog.Title>{title}</AlertDialog.Title>\n        <AlertDialog.Description size=\"2\">{description ?? \"This is recorded with your name.\"}</AlertDialog.Description>\n        {reason && (\n          <Flex direction=\"column\" gap=\"1\" mt=\"3\" asChild>\n            <label>\n              <Text size=\"2\" weight=\"medium\">{reason.label}</Text>\n              <TextArea value={text} onChange={(e) => setText(e.target.value)} placeholder={reason.placeholder} required />\n            </label>\n          </Flex>\n        )}\n        {error && (\n          <Callout.Root color=\"red\" role=\"alert\" mt=\"3\" size=\"1\"><Callout.Text>{error}</Callout.Text></Callout.Root>\n        )}\n        <Flex gap=\"3\" mt=\"4\" justify=\"end\">\n          <AlertDialog.Cancel><Button variant=\"soft\" color=\"gray\">Cancel</Button></AlertDialog.Cancel>\n          <Button color={tone === \"danger\" ? \"red\" : undefined} onClick={confirm} disabled={blocked} loading={busy}>\n            {confirmLabel}\n          </Button>\n        </Flex>\n      </AlertDialog.Content>\n    </AlertDialog.Root>\n  );\n}\n",
      "type": "registry:component",
      "target": "erp-ui/components/confirm-dialog/ConfirmDialog.tsx"
    }
  ],
  "type": "registry:component"
}