From 9f40102d6a3ee6f0e98c19e9afb0a17329e1a7da Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Tue, 21 Jul 2026 13:42:11 +0700 Subject: [PATCH] fix(ui): keep white on the destructive fill readable in both themes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four states were under AA, not one: the dark badge and button at rest (3.71), the light badge on hover (4.34) and the dark button on hover (4.40), plus the dark destructive menu item (4.01). Badge and button had also drifted apart — one faded to 80% on hover, the other to 90% — so each failed in a different theme. The token itself cannot be retuned: sweeping its lightness finds no value that serves both roles, because the same red is a fill under white text and the text colour on the page. Darkening it for the fill would break the twenty-four text-destructive call sites. Tune the fill's opacity per theme instead, keeping white text and adding no tokens. A translucent red composites toward what is behind it, so the same fade darkens it on the dark ground and lightens it on the light one: full strength on light fading to 90% on hover, 80% on dark dropping to 70%. Hover now raises contrast in both themes. All four states land between 5.04 and 6.28, verified on a built bundle against every dark surface token (worst case 5.13 on secondary). The dark destructive menu item drops its focus wash from 20% to 10%, matching light and restoring the label to 4.52 at the cost of a fainter highlight. Co-Authored-By: Claude Fable 5 --- frontend/src/components/ui/badge.tsx | 7 ++++++- frontend/src/components/ui/button.tsx | 6 +++++- frontend/src/components/ui/context-menu.tsx | 2 +- frontend/src/components/ui/dropdown-menu.tsx | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index 7b0fa1a1..565a1580 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -18,7 +18,12 @@ const badgeVariants = cva( variant: { blue: 'border-blue-500/20 bg-blue-500/10 text-blue-700 hover:bg-blue-500/20 dark:text-blue-400', default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + // The dark fill is translucent on purpose: composited over the dark + // ground it darkens the red, which is what keeps the white foreground + // above AA (opaque it measures 3.71). Alphas differ per theme because + // the same fade lightens the red over a white ground. + destructive: + 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/90 dark:bg-destructive/80 dark:hover:bg-destructive/70', green: 'border-green-500/20 bg-green-500/10 text-green-800 hover:bg-green-500/20 dark:text-green-400', orange: 'border-orange-500/20 bg-orange-500/10 text-orange-700 hover:bg-orange-500/20 dark:text-orange-400', outline: 'text-foreground', diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 6b96648f..1972d2ff 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -24,7 +24,11 @@ const buttonVariants = cva( }, variant: { default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90', + // Translucent dark fill on purpose — over the dark ground it darkens + // the red, keeping the white foreground above AA (opaque it is 3.71). + // Mirrors the destructive Badge; keep the two in step. + destructive: + 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 dark:bg-destructive/80 dark:hover:bg-destructive/70', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', diff --git a/frontend/src/components/ui/context-menu.tsx b/frontend/src/components/ui/context-menu.tsx index d6aab07e..9f8da8a9 100644 --- a/frontend/src/components/ui/context-menu.tsx +++ b/frontend/src/components/ui/context-menu.tsx @@ -75,7 +75,7 @@ function ContextMenuItem({ return ( svg]:size-4 [&>svg]:shrink-0', + 'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/10 relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0', inset && 'pl-8', className, )}