mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(settings): passkey list responsive — card view at < md
Phase A5 / B-mobile HIGH: at 393px the passkey table rendered with `min-w-[620px]` inside `overflow-x-auto`, which hid the right-most column entirely — including the destructive delete action. The audit flagged this as a discoverability regression: a user on mobile could not see, let alone tap, the "remove this passkey" button. Fix: render the desktop table only at `≥ md` (`hidden md:block`), and at `< md` paint a card list (`<ul data-testid="passkeys-mobile- list">`) where every passkey's name, device type, backup status, created date, and the delete action are all visible without scrolling. The mobile delete button uses `min-h-11 min-w-11` (44px) to meet WCAG 2.5.5; desktop keeps its denser `h-8 w-8` because mouse pointers don't need 44px. Findings ID: A5/HIGH `/settings/account` — passkey table responsive. Verification: 847 / 847 unit tests pass (was 823; +24 from prior fix-3 sibling commits unrelated to this change), lint clean (13 pre-existing warnings unchanged), typecheck regression-free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8370b2d107
commit
00f8cd556d
1 file changed
+81
-2
@@ -568,8 +568,15 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{t("settings.passkeysDescription")}
|
||||
</p>
|
||||
<div className="border-border mt-3 overflow-x-auto rounded-lg border">
|
||||
<table className="w-full min-w-[620px] text-sm">
|
||||
{/* Phase A5 / B-mobile HIGH: the passkey table previously
|
||||
rendered with `min-w-[620px]` inside `overflow-x-auto`,
|
||||
which on a 393px viewport hid the right-most column —
|
||||
including the destructive delete action. Render the desktop
|
||||
table only at `≥ md`, and at `< md` paint a card-list where
|
||||
every passkey's name, device type, backup status, created
|
||||
date, and delete action are all visible without scrolling. */}
|
||||
<div className="border-border mt-3 hidden overflow-x-auto rounded-lg border md:block">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/40 text-muted-foreground border-b text-xs">
|
||||
<th className="px-3 py-2 text-left font-medium">
|
||||
@@ -618,6 +625,7 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
size="icon"
|
||||
className="text-destructive h-8 w-8"
|
||||
disabled={deletePasskey.isPending}
|
||||
aria-label={t("settings.deletePasskey")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -650,6 +658,77 @@ function PasskeyListSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile card list — < md only. Each passkey gets its own
|
||||
card so the delete action stays visible and tap-targetable
|
||||
(44px) without the user having to horizontally scroll
|
||||
through a wide table. */}
|
||||
<ul
|
||||
className="mt-3 space-y-2 md:hidden"
|
||||
data-testid="passkeys-mobile-list"
|
||||
>
|
||||
{passkeys.map((pk) => (
|
||||
<li
|
||||
key={pk.id}
|
||||
className="bg-card border-border rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{pk.name}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{DEVICE_TYPE_LABELS[pk.credentialDeviceType] ??
|
||||
pk.credentialDeviceType}
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
|
||||
<Badge
|
||||
variant={pk.credentialBackedUp ? "secondary" : "outline"}
|
||||
className="text-[11px]"
|
||||
>
|
||||
{pk.credentialBackedUp
|
||||
? t("settings.backedUp")
|
||||
: t("common.no")}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
{formatDate(pk.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive min-h-11 min-w-11"
|
||||
disabled={deletePasskey.isPending}
|
||||
aria-label={t("settings.deletePasskey")}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("settings.deletePasskey")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("settings.deletePasskeyDescription")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => deletePasskey.mutate(pk.id)}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{deleteMsg && (
|
||||
<div
|
||||
role="alert"
|
||||
|
||||
Reference in new issue
Block a user