fix(ui): make row selection checkboxes always visible at muted opacity (#2093)

* fix(ui): make row selection checkboxes always visible at muted opacity

User feedback: bulk-select checkboxes hidden until hover are "sjukt
pilligt": an invisible 16px target forces a precise hover-then-aim per
row, and bulk selection is a primary workflow on list pages.

New CHECKBOX_REVEAL_CLASS in dry-table.tsx: rest at opacity-50, solid on
row hover, focus, coarse pointers, and checked state. Applied to the 8
selection-checkbox sites (transaction inbox, skattekonto inbox, journal
list, invoices, supplier invoices, orders, pending, invoice inbox
workspace). Also fixes a touch bug: TransactionInboxCard and
SkattekontoInboxCard lacked pointer-coarse fallback, leaving unselected
checkboxes permanently invisible on touch devices.

Row action controls (chevrons, quiet links) keep HOVER_REVEAL_CLASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015n8vUx9Nukr8mHC7CVNF7y

* fix(ui): meet WCAG 3:1 on resting selection checkboxes via border-foreground

Skeptic finding: the checkbox primitive's border-input is ~1.4:1 against
the page, so the new opacity-50 resting state composited to ~1.2:1,
under the 3:1 non-text contrast minimum design.md commits to. Putting
border-foreground unconditionally on the 8 reveal checkboxes lands the
resting border at >=3.4:1 in every theme (light 3.41:1, dark 4.24:1,
white-label palettes similar) and the hover/solid state at ~17:1.

The border class lives at call sites, not in CHECKBOX_REVEAL_CLASS,
because the constant is sometimes applied to a borderless wrapper
(InvoiceInboxWorkspace); documented in dry-table.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015n8vUx9Nukr8mHC7CVNF7y

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-09-01 10:39:34 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 05dce83a2b
commit 5f81c0638c
9 changed files with 48 additions and 33 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ import { ToolbarSearch } from '@/components/ui/toolbar-search'
import { Skeleton } from '@/components/ui/skeleton'
import { Dialog, DialogContent, DialogTitle, DialogVeil } from '@/components/ui/dialog'
import { DataListEmpty } from '@/components/ui/data-list'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS, CHECKBOX_REVEAL_CLASS } from '@/components/ui/dry-table'
import { FyPicker } from '@/components/common/FyPicker'
import { ContextPicker } from '@/components/common/ContextPicker'
import { SplitButton, type SplitButtonOption } from '@/components/ui/split-button'
@@ -789,10 +789,10 @@ export default function InvoicesPage() {
onCheckedChange={() => toggleSelect(invoice.id)}
aria-label={t('bulk_select_row')}
className={cn(
'transition-opacity duration-150',
'border-foreground duration-150',
selectedIds.has(invoice.id) || selectedIds.size > 0
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100',
: CHECKBOX_REVEAL_CLASS,
)}
/>
)}
+3 -5
View File
@@ -19,7 +19,7 @@ import { EmptyState } from '@/components/ui/empty-state'
import { Skeleton } from '@/components/ui/skeleton'
import { useToast } from '@/components/ui/use-toast'
import { ContextPicker } from '@/components/common/ContextPicker'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS, CHECKBOX_REVEAL_CLASS } from '@/components/ui/dry-table'
import { cn, formatCurrency, formatDate } from '@/lib/utils'
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
import { useCanWrite } from '@/lib/hooks/use-can-write'
@@ -510,10 +510,8 @@ function OrderRow({
onCheckedChange={() => onToggleSelect(order.id)}
aria-label={t('select_order_aria', { number: order.order_number })}
className={cn(
'absolute -left-5 top-1/2 -translate-y-1/2 transition-opacity duration-150 md:-left-6',
isSelected
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100',
'absolute -left-5 top-1/2 -translate-y-1/2 border-foreground duration-150 md:-left-6',
isSelected ? 'opacity-100' : CHECKBOX_REVEAL_CLASS,
)}
/>
)}
+4 -4
View File
@@ -10,7 +10,7 @@ import { Checkbox } from '@/components/ui/checkbox'
import { DataListEmpty, DataListLoading } from '@/components/ui/data-list'
import { ContextPicker } from '@/components/common/ContextPicker'
import { SegmentedControl } from '@/components/ui/segmented-control'
import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { CHECKBOX_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import {
SlideOver,
SlideOverContent,
@@ -997,7 +997,7 @@ export default function PendingOperationsPage() {
isSelected && 'bg-secondary/40',
)}
>
{/* Hover-revealed selection checkbox (concept .cb) */}
{/* Always-visible selection checkbox (concept .cb) */}
<span
className="w-[18px] shrink-0 pt-1.5"
onClick={(e) => e.stopPropagation()}
@@ -1008,8 +1008,8 @@ export default function PendingOperationsPage() {
onCheckedChange={() => toggleSelected(op.id)}
aria-label={t('select_operation_aria')}
className={cn(
'duration-150',
isSelected ? 'opacity-100' : HOVER_REVEAL_CLASS,
'border-foreground duration-150',
isSelected ? 'opacity-100' : CHECKBOX_REVEAL_CLASS,
)}
/>
)}
+3 -3
View File
@@ -10,7 +10,7 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { ToolbarSearch } from '@/components/ui/toolbar-search'
import { DataListEmpty } from '@/components/ui/data-list'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS, CHECKBOX_REVEAL_CLASS } from '@/components/ui/dry-table'
import { FyPicker } from '@/components/common/FyPicker'
import { ContextPicker } from '@/components/common/ContextPicker'
import { HelpPopover } from '@/components/ui/help-popover'
@@ -588,10 +588,10 @@ export default function SupplierInvoicesPage() {
onCheckedChange={() => toggleSelect(inv.id)}
aria-label={t('bulk_select_row')}
className={cn(
'transition-opacity duration-150',
'border-foreground duration-150',
selectedIds.has(inv.id) || selectedIds.size > 0
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100',
: CHECKBOX_REVEAL_CLASS,
)}
/>
)}