* 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>
43 lines
2.1 KiB
TypeScript
43 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
// Concept dry-table (scenes 9/10): borderless table straight on the panel,
|
|
// uppercase hairline heads, 13px rows, hover-revealed row controls. Shared by
|
|
// the verifikat list and the transaction inbox so the two read as the same
|
|
// instrument. Keep these in sync with .claude/rules/design.md; page-specific
|
|
// tweaks belong at the call site via cn(), not here.
|
|
export const TH_CLASS =
|
|
'px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-[0.07em] text-muted-foreground border-b border-border whitespace-nowrap'
|
|
export const TD_CLASS = 'px-4 py-[11px] border-b border-border align-top'
|
|
export const VTH_CLASS =
|
|
'py-2 pr-4 text-left text-[10.5px] font-medium uppercase tracking-[0.07em] text-muted-foreground border-b border-border'
|
|
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 }) {
|
|
const [open, setOpen] = useState(false)
|
|
useEffect(() => {
|
|
const raf = requestAnimationFrame(() => setOpen(true))
|
|
return () => cancelAnimationFrame(raf)
|
|
}, [])
|
|
return (
|
|
<div
|
|
className="grid transition-[grid-template-rows] duration-300 ease-out"
|
|
style={{ gridTemplateRows: open ? '1fr' : '0fr' }}
|
|
>
|
|
<div className="min-h-0 overflow-hidden">{children}</div>
|
|
</div>
|
|
)
|
|
}
|