Task #531: Redesign DeletionWarningDialog for a more professional look
Scope: artifacts/tx-os/src/pages/admin.tsx — the shared confirm
dialog used for deleting Apps, Services, and Users on the admin
page.
What changed
- Three explicit visual regions instead of a flat stack:
- Header: AlertTriangle icon in a destructive-tinted circle next
to the bold title (which already contains the item name from
the existing translation strings).
- Body: warning copy, then impacted items rendered inside a
sub-card (rounded border + subtle bg + custom bullet dots
instead of `list-disc`), then a destructive-tinted callout
holding the "cannot be undone" hint.
- Footer: separated by a top border and a faint bg tint;
Cancel = ghost (quieter), Confirm/Anyway = destructive (clear
primary action). When `isPending`, a small spinner appears
inside the destructive button.
- Slightly larger max-width (max-w-md), shadow-xl, and overflow-
hidden so the rounded corners read cleanly with the new footer
divider.
- `role="alertdialog"` + `aria-modal="true"` added on the panel.
Behaviour preserved
- Component props, call sites, and all `data-testid` hooks are
unchanged (testId, confirmTestId — covers app-delete-dialog/
-confirm, service-delete-dialog/-confirm, user-delete-dialog/
-confirm).
- No translation keys added; reuses every existing
admin.deleteApp/Service/User.* string.
- Cancel renders before Confirm in the DOM so tab order is
Cancel -> Destructive (no a11y regression). Visual order is
controlled with `justify-end`, which mirrors correctly in RTL.
- Pure UI change: no API/handler/permission logic touched.
Notes
- Architect flagged an earlier `flex-row-reverse` footer for
putting the destructive button first in tab order; fixed before
marking complete.
- Pre-existing TS18046 / TS2345 errors in admin.tsx are unrelated.
This commit is contained in:
@@ -131,7 +131,7 @@ import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useUpload, type UploadResponse } from "@workspace/object-storage-web";
|
||||
import { isBuiltinAppSlug } from "@workspace/db/built-in-apps";
|
||||
import { resolveServiceImageUrl } from "@/lib/image-url";
|
||||
import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power, HelpCircle, Palette, Link2, Hash, Lock, ExternalLink, type LucideIcon } from "lucide-react";
|
||||
import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power, HelpCircle, Palette, Link2, Hash, Lock, ExternalLink, AlertTriangle, type LucideIcon } from "lucide-react";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
|
||||
function resolveAppIcon(name: string): LucideIcon | null {
|
||||
@@ -279,43 +279,91 @@ function DeletionWarningDialog({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
// Task #531: redesigned destructive-confirm dialog. Three clear
|
||||
// regions (header with warning glyph + title, structured body
|
||||
// with sub-card for impacted items + tinted "cannot be undone"
|
||||
// callout, and a footer separated by a divider). All existing
|
||||
// data-testid hooks are preserved so admin tests keep passing.
|
||||
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="glass-panel rounded-3xl p-6 w-full max-w-sm space-y-3"
|
||||
className="glass-panel rounded-3xl w-full max-w-md overflow-hidden shadow-xl"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
data-testid={testId}
|
||||
>
|
||||
<h2 className="font-semibold text-foreground">{title}</h2>
|
||||
{showWarning ? (
|
||||
<>
|
||||
<p className="text-sm text-destructive">{warning}</p>
|
||||
{items.length > 0 && (
|
||||
<ul className="text-sm text-foreground space-y-1 ps-4 list-disc">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">{forceHint}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{emptyBody}</p>
|
||||
)}
|
||||
<div className="flex gap-2 pt-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 p-5 sm:p-6">
|
||||
<div
|
||||
className="shrink-0 w-10 h-10 rounded-full bg-destructive/10 text-destructive flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<AlertTriangle size={20} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pt-0.5">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-foreground leading-snug">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-5 sm:px-6 pb-5 sm:pb-6 space-y-4">
|
||||
{showWarning ? (
|
||||
<>
|
||||
<p className="text-sm text-foreground/90 leading-relaxed">
|
||||
{warning}
|
||||
</p>
|
||||
{items.length > 0 && (
|
||||
<div className="rounded-xl border border-foreground/10 bg-foreground/5 p-3">
|
||||
<ul className="text-sm text-foreground space-y-1.5">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<span
|
||||
className="mt-1.5 inline-block w-1.5 h-1.5 rounded-full bg-foreground/40 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="leading-relaxed">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2.5">
|
||||
<p className="text-xs sm:text-[13px] text-destructive leading-relaxed">
|
||||
{forceHint}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-foreground/85 leading-relaxed">
|
||||
{emptyBody}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer — Cancel first in DOM (and tab order) so the
|
||||
destructive action is never the first focusable target.
|
||||
`justify-end` keeps the destructive button visually on
|
||||
the trailing edge in both LTR and RTL. */}
|
||||
<div className="flex items-center justify-end gap-2 px-5 sm:px-6 py-4 border-t border-foreground/10 bg-foreground/[0.02]">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
variant="ghost"
|
||||
onClick={onCancel}
|
||||
disabled={isPending}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={onConfirm}
|
||||
data-testid={confirmTestId}
|
||||
className="min-w-[110px] gap-2"
|
||||
>
|
||||
{isPending && (
|
||||
<Loader2 size={14} className="animate-spin" aria-hidden="true" />
|
||||
)}
|
||||
{showWarning ? anywayLabel : confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user