Checkout redesign review
14:00 – 14:45
Room Atlas · 3rd floor
JKAMSB
+2
Free, copy-and-go Alert Dialog components built on the SevenUI Alert Dialog primitive.Read the primitive docs.
"use client";
import { LogOutIcon } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialog01() {
return (
<AlertDialog>
<AlertDialogTrigger
render={
<Button variant="outline">
<LogOutIcon aria-hidden="true" data-icon="inline-start" />
Sign out everywhere
</Button>
}
/>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<LogOutIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Sign out of all devices?</AlertDialogTitle>
<AlertDialogDescription>
You will stay signed in here. Your 4 other sessions end right away.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Sign out</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-01pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-01yarn dlx shadcn@latest add @sevenui/component/alert-dialog-01bunx --bun shadcn@latest add @sevenui/component/alert-dialog-01"use client";
import { Trash2Icon } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialog02() {
return (
<AlertDialog>
<AlertDialogTrigger
render={<Button variant="destructive">Delete project</Button>}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20">
<Trash2Icon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Delete Northwind Analytics?</AlertDialogTitle>
<AlertDialogDescription>
All dashboards, saved queries, and scheduled reports in this project
are deleted permanently. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep project</AlertDialogCancel>
<AlertDialogAction variant="destructive">
Delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-02pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-02yarn dlx shadcn@latest add @sevenui/component/alert-dialog-02bunx --bun shadcn@latest add @sevenui/component/alert-dialog-02"use client";
import { FileTextIcon } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialog03() {
return (
<AlertDialog>
<AlertDialogTrigger
render={<Button variant="outline">Close document</Button>}
/>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogMedia>
<FileTextIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Save changes to "Q3 roadmap"?</AlertDialogTitle>
<AlertDialogDescription>
You have 3 unsaved edits since 10:42 AM. If you don't save, they
will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction
variant="ghost"
className="text-destructive hover:text-destructive sm:mr-auto"
>
Don't save
</AlertDialogAction>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Save</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-03pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-03yarn dlx shadcn@latest add @sevenui/component/alert-dialog-03bunx --bun shadcn@latest add @sevenui/component/alert-dialog-03"use client";
import { useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const repository = "acme/storefront";
const impact = [
{ label: "Branches", value: "18" },
{ label: "Open issues", value: "342" },
{ label: "Releases", value: "57" },
];
export default function AlertDialog04() {
const [value, setValue] = useState("");
const confirmed = value.trim() === repository;
return (
<AlertDialog
onOpenChange={(open) => {
if (!open) setValue("");
}}
>
<AlertDialogTrigger
render={<Button variant="destructive">Delete repository</Button>}
/>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Delete {repository}?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes the repository, its wiki, and all
collaborator access. Forks are not affected.
</AlertDialogDescription>
</AlertDialogHeader>
<dl className="grid grid-cols-3 divide-x rounded-lg border bg-muted/40">
{impact.map((item) => (
<div key={item.label} className="flex flex-col gap-0.5 px-3 py-2">
<dt className="text-xs text-muted-foreground">{item.label}</dt>
<dd className="text-sm font-medium tabular-nums">{item.value}</dd>
</div>
))}
</dl>
<div className="grid gap-2">
<Label htmlFor="alert-dialog-04-confirm" className="font-normal">
<span>
Type <span className="font-mono font-medium">{repository}</span>{" "}
to confirm
</span>
</Label>
<Input
id="alert-dialog-04-confirm"
value={value}
onChange={(event) => setValue(event.target.value)}
autoComplete="off"
spellCheck={false}
placeholder={repository}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive" disabled={!confirmed}>
Delete repository
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-04pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-04yarn dlx shadcn@latest add @sevenui/component/alert-dialog-04bunx --bun shadcn@latest add @sevenui/component/alert-dialog-04"use client";
import { CircleCheckIcon, GlobeIcon, RocketIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
const domain = "fieldnotes.studio";
const changes = [
{ label: "Added", value: 3, sign: "+" },
{ label: "Edited", value: 9, sign: "~" },
{ label: "Removed", value: 2, sign: "−" },
];
export default function AlertDialog05() {
const [step, setStep] = useState<"confirm" | "live">("confirm");
const doneRef = useRef<HTMLButtonElement>(null);
// The focused "Publish changes" button unmounts on the step swap, so hand
// focus to the new primary action instead of letting it fall to <body>.
useEffect(() => {
if (step === "live") doneRef.current?.focus();
}, [step]);
return (
<AlertDialog
onOpenChangeComplete={(open) => {
if (!open) setStep("confirm");
}}
>
<AlertDialogTrigger
render={
<Button>
<RocketIcon aria-hidden="true" data-icon="inline-start" />
Publish
</Button>
}
/>
<AlertDialogContent>
{step === "confirm" ? (
<>
<AlertDialogHeader>
<AlertDialogMedia>
<RocketIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Publish to {domain}?</AlertDialogTitle>
<AlertDialogDescription>
These changes replace the live site. Visitors see them within a
minute, and you can roll back from History.
</AlertDialogDescription>
</AlertDialogHeader>
<dl className="grid grid-cols-3 gap-2">
{changes.map((change) => (
<div
key={change.label}
className="flex flex-col gap-0.5 rounded-lg border px-3 py-2"
>
<dt className="text-xs text-muted-foreground">
{change.label}
</dt>
<dd className="text-sm font-medium tabular-nums">
<span aria-hidden="true" className="text-muted-foreground">
{change.sign}
</span>
{change.value} {change.value === 1 ? "page" : "pages"}
</dd>
</div>
))}
</dl>
<AlertDialogFooter>
<AlertDialogCancel>Not yet</AlertDialogCancel>
<Button onClick={() => setStep("live")}>Publish changes</Button>
</AlertDialogFooter>
</>
) : (
<>
<AlertDialogHeader className="animate-in duration-300 ease-out fade-in-0 slide-in-from-bottom-1">
<AlertDialogMedia className="bg-success/10 text-success">
<CircleCheckIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Your site is live</AlertDialogTitle>
<AlertDialogDescription>
14 page changes were published just now.
</AlertDialogDescription>
</AlertDialogHeader>
<p className="flex min-w-0 items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm animate-in duration-300 ease-out fade-in-0">
<GlobeIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="truncate">https://{domain}</span>
</p>
<AlertDialogFooter>
<AlertDialogAction ref={doneRef}>Done</AlertDialogAction>
</AlertDialogFooter>
</>
)}
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-05pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-05yarn dlx shadcn@latest add @sevenui/component/alert-dialog-05bunx --bun shadcn@latest add @sevenui/component/alert-dialog-05"use client";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
type TransferStatus = "idle" | "pending" | "error" | "success";
const recipient = {
name: "Daniel Kim",
email: "daniel@acme.co",
initials: "DK",
};
export default function AlertDialog06() {
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<TransferStatus>("idle");
const [attempts, setAttempts] = useState(0);
const pending = status === "pending";
// Simulated request: the first attempt fails, the retry succeeds.
useEffect(() => {
if (status !== "pending") return;
const id = setTimeout(() => {
if (attempts === 1) {
setStatus("error");
} else {
setStatus("success");
setOpen(false);
}
}, 1200);
return () => clearTimeout(id);
}, [status, attempts]);
const submit = () => {
setAttempts((count) => count + 1);
setStatus("pending");
};
if (status === "success") {
return (
<div className="flex w-full max-w-sm flex-col items-start gap-3">
<Alert role="status" className="border-success/30 bg-success/5">
<CircleCheckIcon aria-hidden="true" className="text-success" />
<AlertTitle>Ownership transferred</AlertTitle>
<AlertDescription>
{recipient.name} now owns the Acme workspace. You are an admin.
</AlertDescription>
</Alert>
<Button
variant="ghost"
size="sm"
onClick={() => {
setStatus("idle");
setAttempts(0);
}}
>
Reset example
</Button>
</div>
);
}
return (
<AlertDialog
open={open}
onOpenChange={(next) => {
if (pending) return;
setOpen(next);
if (!next) {
setStatus("idle");
setAttempts(0);
}
}}
>
<AlertDialogTrigger
render={<Button variant="outline">Transfer ownership</Button>}
/>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Transfer the Acme workspace?</AlertDialogTitle>
<AlertDialogDescription>
The new owner controls billing and can remove any member, including
you. You will become an admin.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex items-center gap-3 rounded-lg border p-3">
<Avatar>
<AvatarFallback>{recipient.initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">
{recipient.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{recipient.email}
</span>
</div>
</div>
{status === "error" ? (
<Alert variant="destructive">
<CircleAlertIcon aria-hidden="true" />
<AlertTitle>Transfer failed</AlertTitle>
<AlertDescription>
{recipient.name} hasn't verified their email yet. Ask them to
check their inbox, then try again.
</AlertDescription>
</Alert>
) : null}
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<Button disabled={pending} aria-busy={pending} onClick={submit}>
{pending ? (
<>
<Spinner data-icon="inline-start" aria-hidden="true" />
Transferring…
</>
) : status === "error" ? (
"Try again"
) : (
"Transfer ownership"
)}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-06pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-06yarn dlx shadcn@latest add @sevenui/component/alert-dialog-06bunx --bun shadcn@latest add @sevenui/component/alert-dialog-06Priya Raman
Ownerpriya@northwind.io
Daniel Okafor
Admindaniel@northwind.io
Lena Fischer
lena@northwind.io
Marco Silva
marco@northwind.io
"use client";
import * as React from "react";
import { UserMinus } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
type Member = {
id: string;
name: string;
email: string;
role: "Owner" | "Admin" | "Member";
projects: number;
};
const initialMembers: Member[] = [
{
id: "m-1",
name: "Priya Raman",
email: "priya@northwind.io",
role: "Owner",
projects: 12,
},
{
id: "m-2",
name: "Daniel Okafor",
email: "daniel@northwind.io",
role: "Admin",
projects: 7,
},
{
id: "m-3",
name: "Lena Fischer",
email: "lena@northwind.io",
role: "Member",
projects: 3,
},
{
id: "m-4",
name: "Marco Silva",
email: "marco@northwind.io",
role: "Member",
projects: 1,
},
];
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("")
.slice(0, 2);
}
export default function AlertDialog07() {
const [members, setMembers] = React.useState(initialMembers);
const [open, setOpen] = React.useState(false);
// Kept after close so the popup content does not blank out mid exit-animation.
const [target, setTarget] = React.useState<Member | null>(null);
const requestRemoval = (member: Member) => {
setTarget(member);
setOpen(true);
};
const confirmRemoval = () => {
if (!target) return;
setMembers((current) => current.filter((m) => m.id !== target.id));
};
return (
<section
aria-labelledby="alert-dialog-07-heading"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-baseline justify-between gap-4 border-b px-4 py-3">
<h3 id="alert-dialog-07-heading" className="text-sm font-medium">
Team members
</h3>
<span className="text-xs text-muted-foreground tabular-nums">
{members.length} of 10 seats
</span>
</header>
<ul className="divide-y">
{members.map((member) => (
<li key={member.id} className="flex items-center gap-3 px-4 py-3">
<Avatar>
<AvatarFallback>{initials(member.name)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<p className="truncate text-sm font-medium">{member.name}</p>
{member.role !== "Member" ? (
<Badge variant="secondary">{member.role}</Badge>
) : null}
</div>
<p className="truncate text-xs text-muted-foreground">
{member.email}
</p>
</div>
{member.role === "Owner" ? (
<span className="text-xs text-muted-foreground">You</span>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => requestRemoval(member)}
aria-label={`Remove ${member.name}`}
>
Remove
</Button>
)}
</li>
))}
</ul>
{members.length < initialMembers.length ? (
<div className="flex items-center justify-between gap-4 border-t px-4 py-2">
<p className="text-xs text-muted-foreground" aria-live="polite">
{initialMembers.length - members.length} removed
</p>
<Button
variant="ghost"
size="sm"
onClick={() => setMembers(initialMembers)}
>
Restore members
</Button>
</div>
) : null}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive">
<UserMinus aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Remove {target?.name}?</AlertDialogTitle>
<AlertDialogDescription>
They lose access to the Northwind workspace right away. Their{" "}
{target?.projects === 1
? "project moves"
: `${target?.projects} projects move`}{" "}
to you, and the seat is freed on your next invoice.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep member</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={confirmRemoval}>
Remove from team
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-07pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-07yarn dlx shadcn@latest add @sevenui/component/alert-dialog-07bunx --bun shadcn@latest add @sevenui/component/alert-dialog-07Current plan
$24 / month
Renews on October 24, 2026
"use client";
import * as React from "react";
import { CalendarClock, Check, Minus } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
const losses = [
"Unlimited projects drop to 3 active projects",
"Version history shrinks from 90 days to 7",
"Custom domains are unpublished",
];
const periodEnd = "October 24, 2026";
export default function AlertDialog08() {
const [cancelled, setCancelled] = React.useState(false);
const [open, setOpen] = React.useState(false);
return (
<section
aria-labelledby="alert-dialog-08-plan"
className="w-full max-w-sm rounded-xl border bg-card p-5 text-card-foreground"
>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm text-muted-foreground">Current plan</p>
<h3 id="alert-dialog-08-plan" className="text-lg font-semibold">
Studio
</h3>
</div>
{cancelled ? (
<Badge variant="outline">Cancels {periodEnd}</Badge>
) : (
<Badge variant="secondary">Active</Badge>
)}
</div>
<p className="mt-3 text-2xl font-semibold tabular-nums">
$24
<span className="text-sm font-normal text-muted-foreground">
{" "}
/ month
</span>
</p>
<p className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
<CalendarClock aria-hidden="true" className="size-4" />
{cancelled
? `Access continues until ${periodEnd}`
: `Renews on ${periodEnd}`}
</p>
<div className="mt-5 flex flex-col gap-2 sm:flex-row">
{cancelled ? (
<Button className="w-full" onClick={() => setCancelled(false)}>
Resume subscription
</Button>
) : (
<>
<Button variant="outline" className="w-full sm:w-auto sm:flex-1">
Change plan
</Button>
<Button
variant="ghost"
className="w-full sm:w-auto sm:flex-1"
onClick={() => setOpen(true)}
>
Cancel subscription
</Button>
</>
)}
</div>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel your Studio plan?</AlertDialogTitle>
<AlertDialogDescription>
You keep every Studio feature until {periodEnd}. After that, your
workspace moves to the Free plan.
</AlertDialogDescription>
</AlertDialogHeader>
<ul
aria-label="What changes on the Free plan"
className="flex flex-col gap-2 rounded-lg bg-muted/60 p-3 text-sm"
>
{losses.map((loss) => (
<li key={loss} className="flex items-start gap-2">
<Minus
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-muted-foreground"
/>
{loss}
</li>
))}
<li className="flex items-start gap-2 text-muted-foreground">
<Check aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
Your files and projects stay put
</li>
</ul>
<AlertDialogFooter>
<AlertDialogCancel>Keep Studio</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() => setCancelled(true)}
>
Cancel at period end
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-08pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-08yarn dlx shadcn@latest add @sevenui/component/alert-dialog-08bunx --bun shadcn@latest add @sevenui/component/alert-dialog-08"use client";
import * as React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
type Profile = { name: string; handle: string };
const savedProfile: Profile = { name: "Amara Nwosu", handle: "amara" };
const sections = [
{ value: "profile", label: "Profile" },
{ value: "security", label: "Security" },
{ value: "sessions", label: "Sessions" },
];
export default function AlertDialog09() {
const [tab, setTab] = React.useState("profile");
const [saved, setSaved] = React.useState(savedProfile);
const [draft, setDraft] = React.useState(savedProfile);
const [pendingTab, setPendingTab] = React.useState<string | null>(null);
// Remembered separately so the copy stays stable while the dialog animates out.
const [lastRequested, setLastRequested] = React.useState("security");
const dirty = draft.name !== saved.name || draft.handle !== saved.handle;
const handleTabChange = (next: string) => {
if (dirty && tab === "profile" && next !== "profile") {
// Hold the navigation until the user decides what to do with the edits.
setPendingTab(next);
setLastRequested(next);
return;
}
setTab(next);
};
const discardAndLeave = () => {
setDraft(saved);
if (pendingTab) setTab(pendingTab);
};
const pendingLabel =
sections.find((section) => section.value === lastRequested)?.label ??
"another section";
return (
<div className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground">
<Tabs
value={tab}
onValueChange={(value) => handleTabChange(String(value))}
>
<TabsList className="w-full">
{sections.map((section) => (
<TabsTrigger key={section.value} value={section.value}>
{section.label}
{section.value === "profile" && dirty ? (
<>
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-primary"
/>
<span className="sr-only">(unsaved changes)</span>
</>
) : null}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="profile">
<form
className="flex flex-col gap-4 pt-3"
onSubmit={(event) => {
event.preventDefault();
setSaved(draft);
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="alert-dialog-09-name">Display name</Label>
<Input
id="alert-dialog-09-name"
value={draft.name}
onChange={(event) =>
setDraft((d) => ({ ...d, name: event.target.value }))
}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="alert-dialog-09-handle">Username</Label>
<Input
id="alert-dialog-09-handle"
value={draft.handle}
onChange={(event) =>
setDraft((d) => ({ ...d, handle: event.target.value }))
}
/>
<p className="text-xs text-muted-foreground">
Your profile lives at relay.app/{draft.handle || "username"}
</p>
</div>
<div className="flex items-center justify-end gap-2">
<p
aria-live="polite"
className="mr-auto text-xs text-muted-foreground"
>
{dirty ? "Unsaved changes" : "All changes saved"}
</p>
<Button
type="button"
variant="ghost"
disabled={!dirty}
onClick={() => setDraft(saved)}
>
Reset
</Button>
<Button type="submit" disabled={!dirty}>
Save
</Button>
</div>
</form>
</TabsContent>
<TabsContent
value="security"
className="pt-3 text-sm text-muted-foreground"
>
Two-factor authentication is on. Your password was last changed 41
days ago.
</TabsContent>
<TabsContent
value="sessions"
className="pt-3 text-sm text-muted-foreground"
>
You are signed in on 2 devices: a MacBook Pro in Lagos and an iPhone
15.
</TabsContent>
</Tabs>
<AlertDialog
open={pendingTab !== null}
onOpenChange={(open) => {
if (!open) setPendingTab(null);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Leave without saving?</AlertDialogTitle>
<AlertDialogDescription>
Your edits to the profile have not been saved. If you open{" "}
{pendingLabel} now, they will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={discardAndLeave}>
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-09pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-09yarn dlx shadcn@latest add @sevenui/component/alert-dialog-09bunx --bun shadcn@latest add @sevenui/component/alert-dialog-092 selected
"use client";
import * as React from "react";
import { FileImage, FileSpreadsheet, FileText, Trash2 } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
type FileRow = {
id: string;
name: string;
kind: "doc" | "sheet" | "image";
sizeMb: number;
modified: string;
};
const initialFiles: FileRow[] = [
{
id: "f-1",
name: "Q3 board deck.pdf",
kind: "doc",
sizeMb: 18.4,
modified: "Sep 21",
},
{
id: "f-2",
name: "Revenue model v7.xlsx",
kind: "sheet",
sizeMb: 2.1,
modified: "Sep 19",
},
{
id: "f-3",
name: "Offsite venue photo.jpg",
kind: "image",
sizeMb: 6.8,
modified: "Sep 12",
},
{
id: "f-4",
name: "Hiring plan draft.docx",
kind: "doc",
sizeMb: 0.4,
modified: "Aug 30",
},
{
id: "f-5",
name: "Logo exploration.png",
kind: "image",
sizeMb: 11.2,
modified: "Aug 22",
},
];
const icons = { doc: FileText, sheet: FileSpreadsheet, image: FileImage };
export default function AlertDialog10() {
const [files, setFiles] = React.useState(initialFiles);
const [selected, setSelected] = React.useState<string[]>(["f-3", "f-5"]);
const [open, setOpen] = React.useState(false);
// Snapshot of what the dialog is about, frozen while it is open.
const [doomed, setDoomed] = React.useState<FileRow[]>([]);
const allSelected = files.length > 0 && selected.length === files.length;
const someSelected = selected.length > 0 && !allSelected;
const toggle = (id: string, checked: boolean) => {
setSelected((current) =>
checked ? [...current, id] : current.filter((value) => value !== id),
);
};
const requestDelete = () => {
setDoomed(files.filter((file) => selected.includes(file.id)));
setOpen(true);
};
const confirmDelete = () => {
const ids = new Set(doomed.map((file) => file.id));
setFiles((current) => current.filter((file) => !ids.has(file.id)));
setSelected([]);
};
const freed = doomed.reduce((sum, file) => sum + file.sizeMb, 0);
const shown = doomed.slice(0, 3);
const hidden = doomed.length - shown.length;
return (
<div className="w-full max-w-md overflow-hidden rounded-xl border bg-card text-card-foreground">
<div className="flex h-12 items-center gap-3 border-b px-4">
<Checkbox
aria-label="Select all files"
checked={allSelected}
indeterminate={someSelected}
disabled={files.length === 0}
onCheckedChange={(checked) =>
setSelected(checked ? files.map((file) => file.id) : [])
}
/>
<p className="flex-1 text-sm font-medium" aria-live="polite">
{selected.length > 0
? `${selected.length} selected`
: "Shared with Finance"}
</p>
<Button
variant="destructive"
size="sm"
disabled={selected.length === 0}
onClick={requestDelete}
>
<Trash2 aria-hidden="true" />
Delete
</Button>
</div>
{files.length === 0 ? (
<div className="flex flex-col items-center gap-3 px-4 py-10 text-center">
<p className="text-sm text-muted-foreground">This folder is empty.</p>
<Button
variant="outline"
size="sm"
onClick={() => setFiles(initialFiles)}
>
Restore sample files
</Button>
</div>
) : (
<ul>
{files.map((file) => {
const Icon = icons[file.kind];
const checked = selected.includes(file.id);
const checkboxId = `alert-dialog-10-${file.id}`;
return (
<li
key={file.id}
data-selected={checked || undefined}
className="flex items-center gap-3 border-b px-4 py-2.5 last:border-b-0 data-selected:bg-muted/60"
>
<Checkbox
id={checkboxId}
checked={checked}
onCheckedChange={(value) => toggle(file.id, value)}
/>
<Icon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<label
htmlFor={checkboxId}
className="min-w-0 flex-1 truncate text-sm"
>
{file.name}
</label>
<span className="hidden text-xs text-muted-foreground sm:inline">
{file.modified}
</span>
<span className="w-14 text-right text-xs text-muted-foreground tabular-nums">
{file.sizeMb.toFixed(1)} MB
</span>
</li>
);
})}
</ul>
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Delete {doomed.length} {doomed.length === 1 ? "file" : "files"}?
</AlertDialogTitle>
<AlertDialogDescription>
Everyone in Finance loses access, and shared links stop working.
This frees {freed.toFixed(1)} MB of storage.
</AlertDialogDescription>
</AlertDialogHeader>
<ul
aria-label="Files to delete"
className="flex flex-col gap-1.5 rounded-lg border p-3 text-sm"
>
{shown.map((file) => {
const Icon = icons[file.kind];
return (
<li key={file.id} className="flex items-center gap-2">
<Icon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="truncate">{file.name}</span>
</li>
);
})}
{hidden > 0 ? (
<li className="pl-6 text-muted-foreground">and {hidden} more</li>
) : null}
</ul>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={confirmDelete}>
Delete permanently
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-10pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-10yarn dlx shadcn@latest add @sevenui/component/alert-dialog-10bunx --bun shadcn@latest add @sevenui/component/alert-dialog-1042 employees, $186,420.00 total. Draft saved at 09:14.
"use client";
import * as React from "react";
import { LockIcon, TimerIcon } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
const WARNING_SECONDS = 60;
function formatTime(seconds: number) {
const minutes = Math.floor(seconds / 60);
return `${minutes}:${String(seconds % 60).padStart(2, "0")}`;
}
export default function AlertDialog11() {
const [open, setOpen] = React.useState(false);
const [remaining, setRemaining] = React.useState(WARNING_SECONDS);
const [signedOut, setSignedOut] = React.useState(false);
// Counts down only while the warning is showing; the interval is cleared on close.
React.useEffect(() => {
if (!open) return;
const id = setInterval(() => {
setRemaining((seconds) => Math.max(0, seconds - 1));
}, 1000);
return () => clearInterval(id);
}, [open]);
React.useEffect(() => {
if (open && remaining === 0) {
setOpen(false);
setSignedOut(true);
}
}, [open, remaining]);
const warn = () => {
setRemaining(WARNING_SECONDS);
setOpen(true);
};
return (
<section
aria-labelledby="alert-dialog-11-heading"
className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground"
>
{signedOut ? (
<div className="flex flex-col items-center gap-3 py-4 text-center">
<div className="flex size-10 items-center justify-center rounded-full bg-muted">
<LockIcon aria-hidden="true" className="size-5" />
</div>
<div className="flex flex-col gap-1">
<h3 id="alert-dialog-11-heading" className="font-medium">
You were signed out
</h3>
<p className="text-sm text-balance text-muted-foreground">
We ended your session after 15 minutes without activity.
</p>
</div>
<Button size="sm" onClick={() => setSignedOut(false)}>
Sign in again
</Button>
</div>
) : (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<h3 id="alert-dialog-11-heading" className="font-medium">
Payroll run · October
</h3>
<p className="text-sm text-muted-foreground">
42 employees, $186,420.00 total. Draft saved at 09:14.
</p>
</div>
<Button
variant="outline"
size="sm"
className="self-start"
onClick={warn}
>
<TimerIcon aria-hidden="true" data-icon="inline-start" />
Simulate 14 minutes idle
</Button>
</div>
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<TimerIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Are you still there?</AlertDialogTitle>
<AlertDialogDescription>
For your security, you will be signed out soon. Your payroll draft
is saved either way.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex flex-col items-center gap-2">
<p
role="timer"
aria-label={`${remaining} seconds until sign out`}
className="text-3xl font-semibold tabular-nums"
>
{formatTime(remaining)}
</p>
<Progress
aria-label="Time left before sign out"
value={(remaining / WARNING_SECONDS) * 100}
className="w-full [&_[data-slot=progress-indicator]]:transition-[width] [&_[data-slot=progress-indicator]]:duration-1000 [&_[data-slot=progress-indicator]]:ease-linear"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setSignedOut(true)}>
Sign out
</AlertDialogCancel>
<AlertDialogAction>Stay signed in</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-11pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-11yarn dlx shadcn@latest add @sevenui/component/alert-dialog-11bunx --bun shadcn@latest add @sevenui/component/alert-dialog-1114:00 – 14:45
Room Atlas · 3rd floor
"use client";
import * as React from "react";
import { CalendarX2, Clock, MapPin } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
const attendees = [
{ initials: "JK", name: "Jonas Keller" },
{ initials: "AM", name: "Aiko Mori" },
{ initials: "SB", name: "Sofia Barros" },
{ initials: "TW", name: "Theo Walsh" },
{ initials: "RN", name: "Ruth Ndlovu" },
];
export default function AlertDialog12() {
const [notify, setNotify] = React.useState(true);
const [message, setMessage] = React.useState("");
const [status, setStatus] = React.useState<"scheduled" | "cancelled">(
"scheduled",
);
const [notified, setNotified] = React.useState(0);
const cancelled = status === "cancelled";
return (
<article
aria-labelledby="alert-dialog-12-title"
className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex items-start gap-3">
<div
aria-hidden="true"
className="flex w-12 shrink-0 flex-col items-center rounded-lg border py-1.5"
>
<span className="text-[0.65rem] font-medium tracking-wide text-muted-foreground uppercase">
Thu
</span>
<span className="text-lg leading-tight font-semibold tabular-nums">
26
</span>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3
id="alert-dialog-12-title"
className="truncate font-medium data-[cancelled=true]:text-muted-foreground data-[cancelled=true]:line-through"
data-cancelled={cancelled}
>
Checkout redesign review
</h3>
{cancelled ? <Badge variant="destructive">Cancelled</Badge> : null}
</div>
<p className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
<Clock aria-hidden="true" className="size-3.5" />
14:00 – 14:45
</p>
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<MapPin aria-hidden="true" className="size-3.5" />
Room Atlas · 3rd floor
</p>
</div>
</div>
<div className="mt-4 flex items-center justify-between gap-3">
<AvatarGroup
aria-label={`${attendees.length} attendees`}
className="-space-x-1"
>
{attendees.slice(0, 3).map((person) => (
<Avatar key={person.initials}>
<AvatarFallback title={person.name}>
{person.initials}
</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount>+{attendees.length - 3}</AvatarGroupCount>
</AvatarGroup>
<AlertDialog>
{cancelled ? (
<Button
variant="ghost"
size="sm"
onClick={() => setStatus("scheduled")}
>
Restore event
</Button>
) : (
<AlertDialogTrigger
render={
<Button variant="outline" size="sm">
<CalendarX2 aria-hidden="true" />
Cancel event
</Button>
}
/>
)}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Cancel Checkout redesign review?
</AlertDialogTitle>
<AlertDialogDescription>
The event is removed from {attendees.length} calendars and Room
Atlas is released for Thursday 14:00.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-4">
<Label htmlFor="alert-dialog-12-notify" className="font-normal">
Email attendees about the cancellation
</Label>
<Switch
id="alert-dialog-12-notify"
checked={notify}
onCheckedChange={setNotify}
/>
</div>
{notify ? (
<div className="flex flex-col gap-2">
<Label htmlFor="alert-dialog-12-message" className="sr-only">
Note to attendees
</Label>
<Textarea
id="alert-dialog-12-message"
placeholder="Add a note, e.g. moving this to next week's sync."
value={message}
onChange={(event) => setMessage(event.target.value)}
maxLength={280}
/>
</div>
) : null}
</div>
<AlertDialogFooter>
<AlertDialogCancel>Keep event</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() => {
setNotified(notify ? attendees.length : 0);
setStatus("cancelled");
setMessage("");
}}
>
{notify ? "Cancel and notify" : "Cancel quietly"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<p
aria-live="polite"
className="mt-3 text-xs text-muted-foreground empty:hidden"
>
{cancelled
? notified > 0
? `${notified} attendees were emailed.`
: "Attendees were not notified."
: ""}
</p>
</article>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-12pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-12yarn dlx shadcn@latest add @sevenui/component/alert-dialog-12bunx --bun shadcn@latest add @sevenui/component/alert-dialog-12Merino crew sweater
Oat · M
Ribbed wool beanie
Charcoal
Cotton ankle socks
3-pack · 39–42
"use client";
import * as React from "react";
import { Minus, Plus, Truck, X } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Progress, ProgressLabel } from "@/components/ui/progress";
type Line = {
id: string;
name: string;
variant: string;
price: number;
quantity: number;
};
const FREE_SHIPPING_AT = 75;
const SHIPPING_FEE = 6.95;
const initialCart: Line[] = [
{
id: "l-1",
name: "Merino crew sweater",
variant: "Oat · M",
price: 48,
quantity: 1,
},
{
id: "l-2",
name: "Ribbed wool beanie",
variant: "Charcoal",
price: 18,
quantity: 1,
},
{
id: "l-3",
name: "Cotton ankle socks",
variant: "3-pack · 39–42",
price: 14,
quantity: 1,
},
];
const money = (value: number) => `$${value.toFixed(2)}`;
const subtotalOf = (lines: Line[]) =>
lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
export default function AlertDialog13() {
const [cart, setCart] = React.useState(initialCart);
const [open, setOpen] = React.useState(false);
// The change waiting for confirmation, plus what it would leave behind.
const [pending, setPending] = React.useState<{
next: Line[];
label: string;
subtotal: number;
} | null>(null);
const subtotal = subtotalOf(cart);
const qualifies = subtotal >= FREE_SHIPPING_AT;
// Only interrupt when the edit costs the shopper their free shipping.
const applyChange = (next: Line[], label: string) => {
const nextSubtotal = subtotalOf(next);
if (qualifies && nextSubtotal < FREE_SHIPPING_AT) {
setPending({ next, label, subtotal: nextSubtotal });
setOpen(true);
return;
}
setCart(next);
};
const setQuantity = (line: Line, quantity: number) => {
const next =
quantity === 0
? cart.filter((item) => item.id !== line.id)
: cart.map((item) =>
item.id === line.id ? { ...item, quantity } : item,
);
applyChange(
next,
quantity === 0
? `Removing the ${line.name}`
: `Lowering the ${line.name} to ${quantity}`,
);
};
const shortfall = pending ? FREE_SHIPPING_AT - pending.subtotal : 0;
return (
<section
aria-labelledby="alert-dialog-13-heading"
className="w-full max-w-sm rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-baseline justify-between border-b px-4 py-3">
<h3 id="alert-dialog-13-heading" className="text-sm font-medium">
Your bag
</h3>
<span className="text-sm tabular-nums">{money(subtotal)}</span>
</header>
<div className="border-b px-4 py-3">
<Progress
value={Math.min(100, (subtotal / FREE_SHIPPING_AT) * 100)}
className="gap-2"
>
<ProgressLabel className="flex items-center gap-1.5 text-xs font-normal text-muted-foreground">
<Truck aria-hidden="true" className="size-3.5" />
{qualifies
? "Free standard shipping unlocked"
: `${money(FREE_SHIPPING_AT - subtotal)} away from free shipping`}
</ProgressLabel>
</Progress>
</div>
{cart.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-muted-foreground">
Your bag is empty.{" "}
<button
type="button"
className="rounded-sm underline underline-offset-4 outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
onClick={() => setCart(initialCart)}
>
Restore items
</button>
</p>
) : (
<ul className="divide-y">
{cart.map((line) => (
<li key={line.id} className="flex gap-3 px-4 py-3">
<img
src="/placeholder.svg"
alt=""
className="size-14 shrink-0 rounded-md border bg-muted object-cover"
/>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{line.name}</p>
<p className="text-xs text-muted-foreground">
{line.variant}
</p>
</div>
<Button
variant="ghost"
size="icon-xs"
aria-label={`Remove ${line.name}`}
onClick={() => setQuantity(line, 0)}
>
<X aria-hidden="true" />
</Button>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon-xs"
aria-label={`Decrease ${line.name} quantity`}
onClick={() => setQuantity(line, line.quantity - 1)}
>
<Minus aria-hidden="true" />
</Button>
<span className="w-6 text-center text-sm tabular-nums">
{line.quantity}
</span>
<Button
variant="outline"
size="icon-xs"
aria-label={`Increase ${line.name} quantity`}
onClick={() => setQuantity(line, line.quantity + 1)}
>
<Plus aria-hidden="true" />
</Button>
</div>
<span className="text-sm tabular-nums">
{money(line.price * line.quantity)}
</span>
</div>
</div>
</li>
))}
</ul>
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<Truck aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>Lose free shipping?</AlertDialogTitle>
<AlertDialogDescription>
{pending?.label} drops your bag to {money(pending?.subtotal ?? 0)}
, which is {money(shortfall)} under the {money(FREE_SHIPPING_AT)}{" "}
minimum. A {money(SHIPPING_FEE)} shipping fee would apply.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep as is</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (pending) setCart(pending.next);
}}
>
Update bag
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-dialog-13pnpm dlx shadcn@latest add @sevenui/component/alert-dialog-13yarn dlx shadcn@latest add @sevenui/component/alert-dialog-13bunx --bun shadcn@latest add @sevenui/component/alert-dialog-13