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:
riyadhafraa
2026-05-13 15:49:47 +00:00
parent 60fbd3dd84
commit 21ccb6c426
+71 -23
View File
@@ -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>