fix(buyer-tasks): make card layout mobile-friendly

At <=640px the buyer-tasks card content was crushed by the action
column sitting beside it: the PO number badge broke mid-string
('PO-2026-08-042' wrapping to 4 lines), supplier names wrapped to
6 lines, and the new 'Sent to ...' caption overlapped the PO
badge on the PO Sent tab.

Three responsive tweaks, all using Tailwind breakpoint utilities:

1. Outer card container switches from 'flex items-start' to
   'flex flex-col sm:flex-row sm:items-start'. The action column
   drops below the info column on mobile, giving the info all the
   available width and the buttons full-width tap targets.

2. PO number badge gains 'whitespace-nowrap'. The badge stays
   atomic; the parent's 'flex-wrap' carries the whole badge to a
   new line when it doesn't fit.

3. PO Sent right column: buttons stack vertically on mobile
   ('flex-col sm:flex-row' + 'flex-1 sm:flex-none' on each
   button) and the caption switches from 'text-right' to
   'text-left sm:text-right' so it sits under the buttons.

Desktop layout is unchanged — verified at 1280px that the
side-by-side buttons, right-aligned caption, and inline single
button all still render exactly as before.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-06 18:21:38 +02:00
parent d2ffbea6bc
commit bf54511db1

View file

@ -67,17 +67,25 @@ export function BuyerTaskGroupCard({
/>
<CardContent className="p-0 pl-3">
<div className="flex items-start gap-3 p-3">
{/* On mobile the action column drops below the info column so
neither gets crushed — the PO number badge no longer wraps
mid-number and the buttons get full-width tap targets. On
`sm:` and up the original side-by-side layout returns. */}
<div className="flex flex-col sm:flex-row sm:items-start gap-3 p-3">
<div className="min-w-0 flex-1 space-y-1.5">
{/* PO number + order number on the top row so they're
both scannable. The PO number is the headline — the
thing the operator needs to find quickly — so it goes
first and gets its own visual weight. Only shown when
a PO has been generated (po_draft / po_sent tabs). */}
a PO has been generated (po_draft / po_sent tabs). The
badge uses `whitespace-nowrap` so a long PO number
(`PO-26071401`) never breaks mid-string when the info
column gets narrow — the parent `flex-wrap` carries
it onto the next line as a whole. */}
<div className="flex items-baseline gap-2 flex-wrap">
{group.poNumber && (
<span
className="inline-flex items-center gap-1 rounded-md border border-warn/40 bg-warn px-2 py-0.5 font-mono text-xs font-bold text-warn-foreground ring-1 ring-inset ring-warn/20 dark:bg-warn/20 dark:border-warn/30 dark:text-warn dark:ring-warn/20"
className="inline-flex items-center gap-1 whitespace-nowrap rounded-md border border-warn/40 bg-warn px-2 py-0.5 font-mono text-xs font-bold text-warn-foreground ring-1 ring-inset ring-warn/20 dark:bg-warn/20 dark:border-warn/30 dark:text-warn dark:ring-warn/20"
data-testid={`buyer-task-po-number-${group.groupId}`}
>
<FileText className="h-3 w-3" />
@ -88,7 +96,7 @@ export function BuyerTaskGroupCard({
{group.orderNumber}
</span>
<span
className="text-sm font-semibold"
className="min-w-0 text-sm font-semibold"
// Tint the supplier name with the same colour as the
// stripe so the supplier is recognisable even when the
// card content overlaps with neighbours.
@ -117,13 +125,20 @@ export function BuyerTaskGroupCard({
// recorded `sentAt` (legacy rows predating email_payload,
// or a fresh po_draft that got flipped straight to
// po_sent without a successful send).
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-2">
//
// Mobile (below `sm:`): the buttons stack vertically and
// stretch to the full card width so the tap target is easy
// to hit; the caption aligns left to sit under the buttons
// naturally. On `sm:` and up the original side-by-side
// button row + right-aligned caption returns.
<div className="flex flex-col items-stretch sm:items-end gap-2">
<div className="flex flex-col sm:flex-row gap-2">
<Button
type="button"
onClick={() => onResend?.(group)}
disabled={disabled || !onResend}
className={cn(
"flex-1 sm:flex-none",
RESEND_DEFAULT_CLASS,
group.poStatus === "mail_failed" && RESEND_FAILED_CLASS,
)}
@ -136,7 +151,10 @@ export function BuyerTaskGroupCard({
type="button"
onClick={() => onAction(group)}
disabled={disabled}
className={ACTION_CLASS[group.step]}
className={cn(
"flex-1 sm:flex-none",
ACTION_CLASS[group.step],
)}
data-testid={`buyer-task-action-${group.groupId}-${group.step}`}
>
{ACTION_LABEL[group.step]}
@ -144,7 +162,7 @@ export function BuyerTaskGroupCard({
</div>
{group.emailPayload?.sentAt ? (
<div
className="text-xs text-muted-foreground text-right leading-tight"
className="text-xs text-muted-foreground text-left sm:text-right leading-tight"
data-testid={`buyer-task-sent-info-${group.groupId}`}
>
{group.emailPayload.to ? (
@ -169,12 +187,16 @@ export function BuyerTaskGroupCard({
) : null}
</div>
) : (
// needs_po / po_draft: single button (unchanged behaviour).
// needs_po / po_draft: single button. On mobile the button
// stretches full-width below the info column (better tap
// target, gives the info column all the available width);
// on `sm:` it sits inline to the right of the info.
<Button
type="button"
onClick={() => onAction(group)}
disabled={disabled}
className={cn(
"sm:self-auto self-stretch",
ACTION_CLASS[group.step],
group.poStatus === "mail_failed" &&
"bg-destructive hover:bg-destructive/90 text-destructive-foreground",