fix(ui): reach touch-only actions, confirm before posting, drop a dead ring (#1280)

* fix(ui): reach touch-only actions, confirm before posting, drop a dead ring

Three defects from the UI craft audit where the interface is wrong, not
just inconsistent.

Unreachable on touch: 'Markera klar' on deadlines and the bulk-select
checkbox on /pending were 'opacity-0 group-hover:opacity-100'. Coarse
pointers never fire hover and DeadlineForm has no completion control, so
on a phone there was no way to mark a deadline done at all. Factored the
reveal into HOVER_REVEAL_CLASS, which adds pointer-coarse:opacity-100.

Unguarded ledger writes: 'Bokför' on the journal-entry detail page and on
the invoice detail page posted an immutable verifikat on one click, one
screen after the list confirmed the identical action. Both now open a
ConfirmDialog describing the outcome up front (convention 10), reusing
the list's indicative voucher-number prefetch.

Dead ring: SummaryCard emitted ring-1 ring-primary/40 and ring-1
ring-warning/40 on the same element, so the two set the same custom
property and one silently lost. The override is an exception, so it is a
Badge now (conventions 5 and 12).

Also corrects the design.md primitives table, which named ui/table.tsx as
the data-table primitive while 17 files use ui/dry-table.tsx; building a
list page from the documented row produced ~15% taller rows and a
different hover tint, which had already happened twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): let the post-confirm dialog close after a successful commit

ConfirmDialog calls onOpenChange(false) immediately after `await onConfirm()`
resolves, but handleCommit/handleBook clear their in-flight flag in a
`finally` block, so the guard's closure still saw isCommitting/isUpdating as
true and swallowed the close: the dialog would sit open over a booking that
had already succeeded.

The guard was redundant as well as wrong. ConfirmDialog already blocks
Radix-initiated closes while pending, via `onOpenChange={(next) => !pending
&& onOpenChange(next)}` on the Dialog itself. Passing the setter directly
matches every other dialog on both pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-29 12:33:36 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 8571f9235b
commit bc5c673f7d
10 changed files with 120 additions and 20 deletions
+2 -5
View File
@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'
import { Deadline } from '@/types'
import { cn } from '@/lib/utils'
import { isDeadlineOverdue } from '@/lib/calendar/utils'
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { deadlineDateLabel } from './DeadlineRow'
import { Pencil } from 'lucide-react'
@@ -92,10 +92,7 @@ export function DeadlineGroupCard({ deadlines, onEdit, onRequestToggle }: Deadli
e.stopPropagation()
onRequestToggle(deadline)
}}
className={cn(
QUIET_LINK_CLASS,
'shrink-0 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100',
)}
className={cn(QUIET_LINK_CLASS, HOVER_REVEAL_CLASS, 'shrink-0')}
>
{t('group_mark_done')}
</button>
+3 -3
View File
@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'
import { Deadline } from '@/types'
import { cn } from '@/lib/utils'
import { isDeadlineOverdue, parseDate, startOfDay } from '@/lib/calendar/utils'
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { HOVER_REVEAL_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import {
CalendarClock,
Check,
@@ -165,7 +165,7 @@ export function DeadlineRow({ deadline, onEdit, onRequestToggle }: DeadlineRowPr
e.stopPropagation()
onRequestToggle(deadline)
}}
className={cn(QUIET_LINK_CLASS, 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100')}
className={cn(QUIET_LINK_CLASS, HOVER_REVEAL_CLASS)}
>
{t('mark_not_done')}
</button>
@@ -176,7 +176,7 @@ export function DeadlineRow({ deadline, onEdit, onRequestToggle }: DeadlineRowPr
e.stopPropagation()
onRequestToggle(deadline)
}}
className={cn(QUIET_LINK_CLASS, 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100')}
className={cn(QUIET_LINK_CLASS, HOVER_REVEAL_CLASS)}
>
{t('group_mark_done')}
</button>
+7
View File
@@ -16,6 +16,13 @@ export const VTD_CLASS = 'py-[7px] pr-4 border-b border-border/60 align-top'
export const QUIET_LINK_CLASS =
'text-[12.5px] text-muted-foreground underline decoration-border underline-offset-4 transition-colors duration-150 hover:text-foreground'
// Row controls that stay out of the way until the row is hovered. Coarse
// pointers never fire hover, so without pointer-coarse: the control would be
// permanently invisible and the action unreachable on touch. Always use this
// constant instead of hand-rolling `opacity-0 group-hover:opacity-100`.
export const HOVER_REVEAL_CLASS =
'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100'
// Animated row expansion (concept vwrap/vinner): grid-rows 0fr -> 1fr on
// mount; the global reduced-motion rule collapses the transition.
export function RowFoldout({ children }: { children: React.ReactNode }) {