diff --git a/ui/src/components/AttentionQueueRow.test.tsx b/ui/src/components/AttentionQueueRow.test.tsx index 14056b9411..313fb95121 100644 --- a/ui/src/components/AttentionQueueRow.test.tsx +++ b/ui/src/components/AttentionQueueRow.test.tsx @@ -203,6 +203,25 @@ describe("AttentionQueueRow", () => { expect(onToggleExpand).toHaveBeenCalledTimes(1); }); + it("exposes the visible expand chevron as an accessible button", () => { + const onToggleExpand = vi.fn(); + render( + , + ); + + const chevronButton = container?.querySelector('button[aria-label="Expand decision"]'); + expect(chevronButton).toBeTruthy(); + expect(chevronButton?.getAttribute("aria-expanded")).toBe("false"); + act(() => chevronButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onToggleExpand).toHaveBeenCalledWith(expect.objectContaining({ id: "a1" })); + }); + it("does not navigate on title click — the title is plain text, not a link", () => { render( { expect(row?.getAttribute("class")).toContain("ring-ring"); }); - it("renders collapsed inline decision verbs in the right-side action area with semantic variants", () => { + it("renders collapsed inline decision verbs in a dedicated action bar with semantic variants", () => { render( { expect(decisionActions?.textContent).toContain("Approve"); expect(decisionActions?.textContent).toContain("Reject"); + // The action bar is its own full-width band (mobile-first) that collapses to + // a right-aligned pill row once the row's container is wide (container query) + // — no longer a stretched right column. const actionArea = decisionActions?.closest('[data-attention-actions="true"]'); - expect(actionArea?.getAttribute("class")).toContain("mt-auto"); - - const controls = decisionActions?.closest('[data-attention-controls="true"]'); - expect(controls?.getAttribute("class")).toContain("self-stretch"); - expect(controls?.getAttribute("class")).toContain("justify-between"); + expect(actionArea?.getAttribute("class")).toContain("@xl:justify-end"); const rowMenu = container?.querySelector('[aria-label="Row actions"]'); expect(rowMenu?.closest('[data-attention-menu="true"]')).toBeTruthy(); @@ -423,7 +441,7 @@ describe("AttentionQueueRow", () => { expect(issuesApi.rejectInteraction).not.toHaveBeenCalled(); }); - it("centers thumbnails beside the full card text stack", () => { + it("renders evidence thumbnails in a centered context row below the text stack", () => { render( 0; + const showOpen = !inline && !!href; + const showRestore = isHidden && !!onRestore; + const showActionBar = showCompact || showOpen || showRestore; + // Left gutter width (chevron + gap) so the stacked content aligns under the + // headline in the wide layout; when narrow, everything runs full-bleed. + const gutterIndent = "@xl:pl-6"; + return (
-
- {/* Clickable header region: toggles expand for inline rows (plan §2/§5). */} -
- {/* Expand affordance / source icon */} - {expandable ? ( - - {expanded ? : } - - ) : ( - - - - )} +
+ {/* Expand affordance / spacer gutter — keeps headlines aligned across the list. */} + {expandable ? ( + + ) : ( + + )} -
-
-
- - - {meta.label} + {/* Content column: a single vertical stack that fills the full width on + mobile (no competing right-hand controls) and reads top-to-bottom. */} +
+ {/* Meta band: identity on the left, recency + overflow on the right. + Not part of the clickable headline, so the menu never toggles it. */} +
+
+ + + {meta.label} + + {sevBadge && ( + + {sevBadge.label} - {sevBadge && ( - - {sevBadge.label} - - )} - {item.relatedIssue?.identifier && ( - e.stopPropagation()} - > - {item.relatedIssue.identifier} - - )} -
- -
- - {item.subject.title ?? meta.label} - -

{detailLine}

- - {item.project && ( -
- -
- )} -
+ )} + {item.relatedIssue?.identifier && ( + e.stopPropagation()} + > + {item.relatedIssue.identifier} + + )}
- {images.length > 0 && } -
-
- - {/* Controls: kept as siblings (not inside the clickable header) so they - never toggle expand and stay valid interactive targets. */} -
-
- {isHidden && snoozedUntil ? ( - - Reappears {reappearLabel(snoozedUntil)} - - ) : ( - {relativeTime(item.activityAt)} - )} - {!isHidden && ( - - - - - - {onSnooze && onSnooze(item, iso)} />} - onDismiss(item)}> - - Dismiss - - {href && ( - <> - - - Open source - - - )} - - - )} +
+ {isHidden && snoozedUntil ? ( + + Reappears {reappearLabel(snoozedUntil)} + + ) : ( + {relativeTime(item.activityAt)} + )} + {!isHidden && ( + + + + + + {onSnooze && onSnooze(item, iso)} />} + onDismiss(item)}> + + Dismiss + + {href && ( + <> + + + Open source + + + )} + + + )} +
-
- {!expanded && onToggleExpand(item)} />} + {/* Headline — the primary expand target for inline rows. Title now wraps + to two lines instead of truncating to a sliver on narrow screens. */} +
+ + {item.subject.title ?? meta.label} + +

{detailLine}

+
-
- {!inline && href && ( - )} - {isHidden && onRestore && ( - )}
-
+ )}
@@ -325,6 +364,14 @@ function compactDecisionAction(item: AttentionItem, verbId: string): CompactDeci return null; } +/** The compact accept/reject verbs a collapsed row can resolve in place. */ +function collectCompactActions(item: AttentionItem): Array<{ action: CompactDecisionAction; label: string; id: string }> { + return item.decisionVerbs.slice(0, 3).flatMap((verb) => { + const action = compactDecisionAction(item, verb.id); + return action ? [{ action, label: verb.label, id: verb.id }] : []; + }); +} + function CompactDecisionActions({ item, companyId, @@ -336,12 +383,7 @@ function CompactDecisionActions({ }) { const queryClient = useQueryClient(); const { pushToast } = useToastActions(); - const actions = item.decisionVerbs - .slice(0, 3) - .flatMap((verb) => { - const action = compactDecisionAction(item, verb.id); - return action ? [{ action, label: verb.label, id: verb.id }] : []; - }); + const actions = collectCompactActions(item); const decision = useMutation({ mutationFn: (action: CompactDecisionAction) => { @@ -387,13 +429,14 @@ function CompactDecisionActions({ if (actions.length === 0) return null; return ( -
+
{actions.map(({ action, id, label }) => (