mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
Signing every other device out, revoking a single session, revoking a trusted device and revoking them all each fired on a single tap. None is undoable — the other devices simply have to sign in again, and a revoked trusted device asks for its second factor next time — while every other destructive surface in the app confirms first. On a shared screen a mis-tap is a support call. A sweep of the rest of the tree found two more hard deletes with neither a confirmation nor an undo: a Coach reminder and a travel entry. Both now confirm. Deleting a document deliberately does not: it soft-deletes and offers an undo, which covers the mis-tap. The four controls share one ConfirmButton rather than four copies of the dialog, so the next destructive control gets the confirmation by using it instead of by someone remembering. Copy is per control and states the actual consequence — no claim of more finality than is true.
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* A destructive settings control that asks first.
|
|
*
|
|
* The security card's "sign out everywhere" and its per-row revoke, and the
|
|
* same pair on trusted devices, fired on a single tap with no confirmation —
|
|
* while every other destructive surface in the app confirms. Signing every
|
|
* other device out is not recoverable by an undo; the other devices simply
|
|
* have to log in again, and on a shared screen a mis-tap is a support call.
|
|
*
|
|
* Wrapping the pattern rather than inlining a fourth dialog is the point: the
|
|
* next destructive control gets the confirmation by using this, instead of by
|
|
* someone remembering to add one.
|
|
*
|
|
* The copy contract matches the rest of the app after the delete-confirmation
|
|
* sweep — say what happens, and claim no more finality than is true. Callers
|
|
* pass their own body text so each control can be specific about its own
|
|
* consequence.
|
|
*/
|
|
|
|
import type { ReactNode } from "react";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
export function ConfirmButton({
|
|
label,
|
|
title,
|
|
body,
|
|
confirmLabel,
|
|
onConfirm,
|
|
pending = false,
|
|
disabled = false,
|
|
size,
|
|
variant = "outline",
|
|
className,
|
|
icon,
|
|
ariaLabel,
|
|
slot,
|
|
}: {
|
|
/** The control's own label, before the dialog opens. */
|
|
label: string;
|
|
title: string;
|
|
body: string;
|
|
/** The dialog's action label. Names the act, not "OK". */
|
|
confirmLabel: string;
|
|
onConfirm: () => void;
|
|
pending?: boolean;
|
|
disabled?: boolean;
|
|
size?: "sm" | "icon";
|
|
variant?: "outline" | "ghost";
|
|
className?: string;
|
|
/** For an icon-only control, where `label` renders as an icon. */
|
|
ariaLabel?: string;
|
|
icon?: ReactNode;
|
|
/** `data-slot` for tests and the design guards. */
|
|
slot?: string;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
|
|
return (
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant={variant}
|
|
size={size}
|
|
className={className}
|
|
aria-label={ariaLabel}
|
|
data-slot={slot}
|
|
disabled={disabled || pending}
|
|
>
|
|
{pending ? (
|
|
<Loader2
|
|
className="size-4 animate-spin motion-reduce:animate-none"
|
|
aria-hidden
|
|
/>
|
|
) : (
|
|
icon
|
|
)}
|
|
{label}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
<AlertDialogDescription>{body}</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
data-slot={slot ? `${slot}-confirm` : undefined}
|
|
disabled={pending}
|
|
aria-busy={pending || undefined}
|
|
onClick={onConfirm}
|
|
>
|
|
{pending && (
|
|
<Loader2 className="mr-1 size-3.5 animate-spin motion-reduce:animate-none" />
|
|
)}
|
|
{confirmLabel}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|