jengacore.shadcn registry · Tailwind v4 · Radix and Base UI
Components
jengacore components · @jengacore/confirm-dialog

ConfirmDialog

Maker-checker and destructive confirmation that repeats the object and its details; optional required reason.

Install

terminal
npx shadcn@latest add @jengacore/confirm-dialog

Without the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/confirm-dialog.json

Source

components/jc/confirm-dialog.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Textarea } from "@/components/ui/textarea"
import { FormField } from "@/components/jc/form-field"

export type ConfirmDetail = { label: string; value: React.ReactNode }

type ConfirmDialogProps = {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description?: React.ReactNode
  details?: ConfirmDetail[]
  /** A verb that names the action: "Suspend participant", "Record funds in". */
  confirmLabel: string
  cancelLabel?: string
  destructive?: boolean
  requireReason?: boolean
  reasonLabel?: string
  reasonHelper?: string
  onConfirm: (reason?: string) => void | Promise<void>
}

/**
 * Maker-checker and destructive confirmation. The title repeats the object ("Suspend Equity Bank?"),
 * the details repeat participant, currency, amount and reference, and a reason can be required
 * (dispute decisions, NDC changes). The confirm button keeps its width while pending.
 */
export function ConfirmDialog(props: ConfirmDialogProps) {
  const { open, onOpenChange } = props
  const [pending, setPending] = React.useState(false)
  return (
    <Dialog open={open} onOpenChange={(next: boolean) => !pending && onOpenChange(next)}>
      {/* The body mounts with the dialog and unmounts with it, so every opening starts clean. */}
      <DialogContent showCloseButton={!pending}>{open ? <ConfirmBody {...props} pending={pending} setPending={setPending} /> : null}</DialogContent>
    </Dialog>
  )
}

function ConfirmBody({
  onOpenChange,
  title,
  description,
  details = [],
  confirmLabel,
  cancelLabel = "Cancel",
  destructive = false,
  requireReason = false,
  reasonLabel = "Reason",
  reasonHelper = "Recorded in the audit trail with your name.",
  onConfirm,
  pending,
  setPending,
}: ConfirmDialogProps & { pending: boolean; setPending: (pending: boolean) => void }) {
  const [reason, setReason] = React.useState("")
  const [error, setError] = React.useState<string | null>(null)

  async function confirm() {
    if (requireReason && reason.trim().length < 5) {
      setError("Write at least a short sentence explaining the decision.")
      return
    }
    setPending(true)
    try {
      await onConfirm(requireReason ? reason.trim() : undefined)
      setPending(false)
      onOpenChange(false)
    } catch (e) {
      setPending(false)
      setError(e instanceof Error ? e.message : "The action failed. Try again or contact the hub operator.")
    }
  }

  return (
    <>
      <DialogHeader>
        <DialogTitle>{title}</DialogTitle>
        {description ? <DialogDescription>{description}</DialogDescription> : null}
      </DialogHeader>
      {details.length ? (
        <dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 rounded-md bg-muted px-4 py-3 text-[13.5px]">
          {details.map((d) => (
            <React.Fragment key={d.label}>
              <dt className="font-mono text-[10.5px] leading-5 tracking-[0.1em] text-muted-foreground uppercase">{d.label}</dt>
              <dd className="min-w-0 font-medium">{d.value}</dd>
            </React.Fragment>
          ))}
        </dl>
      ) : null}
      {requireReason ? (
        <FormField label={reasonLabel} htmlFor="jc-confirm-reason" required helper={reasonHelper} error={error ?? undefined}>
          <Textarea id="jc-confirm-reason" value={reason} onChange={(e) => setReason(e.target.value)} disabled={pending} />
        </FormField>
      ) : error ? (
        <p className="text-[13px] text-destructive">{error}</p>
      ) : null}
      <DialogFooter>
        <Button variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>
          {cancelLabel}
        </Button>
        <Button variant={destructive ? "destructive" : "default"} onClick={confirm} loading={pending}>
          {confirmLabel}
        </Button>
      </DialogFooter>
    </>
  )
}