FN-5925: polish task review tab layout

Refine the task review tab layout for clearer hierarchy and better mobile behavior.

- group the review summary and decision badge into a stacked header layout
- reorganize review items with dedicated selection, status, and metadata regions
- restyle review cards, status badges, auto-merge controls, and body containers for improved spacing and overflow handling
- extend TaskReviewTab coverage for populated layout hooks and mobile CSS regressions

Files changed:
 packages/dashboard/app/components/TaskReviewTab.css     | 220 +++++++++++++++------
 packages/dashboard/app/components/TaskReviewTab.tsx     |  24 ++-
 packages/dashboard/app/components/__tests__/TaskReviewTab.test.tsx    |  32 ++-
 3 files changed, 206 insertions(+), 70 deletions(-)

Fusion-Task-Id: FN-5925

Fusion-Task-Lineage: fdb9d882-abda-48b0-aed0-a1ff4262af8f
This commit is contained in:
gsxdsm
2026-06-02 23:26:59 -07:00
parent a1b7556c7b
commit 8b450506fc
3 changed files with 207 additions and 71 deletions

View File

@@ -1,110 +1,188 @@
.task-review-tab {
display: flex;
flex-direction: column;
gap: var(--space-md);
gap: var(--space-lg);
min-width: 0;
}
.task-review-tab__header {
display: flex;
gap: var(--space-md);
align-items: flex-start;
justify-content: space-between;
align-items: center;
gap: var(--space-lg);
flex-wrap: wrap;
}
.task-review-tab__summary-wrap {
flex: 1 1 20rem;
min-width: 0;
}
.task-review-tab__summary-group {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
flex-direction: column;
align-items: flex-start;
gap: var(--space-xs);
min-width: 0;
}
.task-review-tab__summary {
margin: 0;
color: var(--text-muted);
color: var(--text);
font-family: var(--font-primary);
font-size: 0.9375rem;
font-weight: 600;
line-height: 1.5;
overflow-wrap: anywhere;
}
.task-review-tab__decision {
.task-review-tab__decision,
.task-review-tab__status {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: fit-content;
min-width: 0;
padding: 0 var(--space-sm);
min-height: calc(var(--space-lg) + var(--space-xs));
border-radius: var(--radius-pill);
border: var(--btn-border-width) solid transparent;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.2;
text-transform: capitalize;
}
.task-review-tab__decision--APPROVED {
.task-review-tab__decision--APPROVED,
.task-review-tab__status--addressed {
color: var(--color-success);
background: color-mix(in srgb, var(--color-success) 14%, transparent);
border-color: color-mix(in srgb, var(--color-success) 28%, transparent);
}
.task-review-tab__decision--CHANGES_REQUESTED {
.task-review-tab__decision--CHANGES_REQUESTED,
.task-review-tab__status--failed {
color: var(--color-error);
background: color-mix(in srgb, var(--color-error) 14%, transparent);
border-color: color-mix(in srgb, var(--color-error) 28%, transparent);
}
.task-review-tab__decision--COMMENTED,
.task-review-tab__decision--PENDING,
.task-review-tab__decision--REVIEW_REQUIRED {
.task-review-tab__decision--REVIEW_REQUIRED,
.task-review-tab__status--queued,
.task-review-tab__status--in-progress {
color: var(--color-warning);
background: color-mix(in srgb, var(--color-warning) 16%, transparent);
border-color: color-mix(in srgb, var(--color-warning) 30%, transparent);
}
.task-review-tab__actions {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-end;
gap: var(--space-sm);
flex: 0 1 auto;
}
.task-review-tab__actions .btn {
flex-shrink: 0;
}
.task-review-tab__auto-merge-control {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
min-inline-size: max-content;
gap: var(--space-xs);
min-inline-size: 14rem;
padding: var(--space-sm);
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--surface) 72%, var(--card));
}
.task-review-tab__list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
gap: var(--space-md);
margin: 0;
padding: 0;
list-style: none;
}
.task-review-tab__item {
padding: var(--space-md);
padding: var(--card-padding);
}
.task-review-tab__item-inner {
display: flex;
flex-direction: column;
gap: var(--space-sm);
gap: var(--space-md);
min-width: 0;
}
.task-review-tab__row {
display: grid;
grid-template-columns: auto 1fr auto;
.task-review-tab__direct-item {
display: block;
min-width: 0;
}
.task-review-tab__direct-item--selectable {
cursor: pointer;
}
.task-review-tab__item-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-sm);
align-items: start;
min-width: 0;
}
.task-review-tab__item-selection {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
min-width: 0;
flex: 1 1 auto;
}
.task-review-tab__item-selection input {
margin: 0;
flex-shrink: 0;
}
.task-review-tab__item-summary {
color: var(--text);
font-family: var(--font-primary);
font-size: 0.875rem;
font-weight: 600;
line-height: 1.5;
overflow-wrap: anywhere;
}
.task-review-tab__direct-item {
.task-review-tab__item-meta-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
gap: var(--space-xs);
}
.task-review-tab__body {
margin: 0;
padding: var(--space-sm);
min-width: 0;
max-width: 100%;
padding: var(--space-md);
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-sm);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--text);
font-family: var(--font-primary);
font-size: 0.875rem;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-width: 100%;
font-family: var(--font-primary);
overflow-wrap: anywhere;
overflow-x: auto;
}
.task-review-tab__body.markdown-body {
@@ -124,22 +202,12 @@
max-width: 100%;
}
.task-review-tab__status {
color: var(--text-muted);
text-transform: capitalize;
}
.task-review-tab__status--failed {
color: var(--color-error);
}
.task-review-tab__status--queued,
.task-review-tab__status--in-progress {
color: var(--color-warning);
}
.task-review-tab__status--addressed {
color: var(--color-success);
.task-review-tab__meta,
.task-review-tab__empty,
.task-review-tab__error {
font-family: var(--font-primary);
font-size: 0.75rem;
line-height: 1.5;
}
.task-review-tab__meta,
@@ -147,18 +215,35 @@
color: var(--text-muted);
}
.task-review-tab__refresh-meta {
display: inline-flex;
align-items: center;
.task-review-tab__refresh-meta,
.task-review-tab__empty,
.task-review-tab__error {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--surface) 72%, var(--card));
}
.task-review-tab__refresh-meta {
min-width: 0;
}
.task-review-tab__refresh-meta .status-dot {
flex-shrink: 0;
margin-top: var(--space-xs);
}
.task-review-tab__empty {
overflow-wrap: anywhere;
}
.task-review-tab__error {
color: var(--color-error);
border-color: color-mix(in srgb, var(--color-error) 26%, var(--border));
background: color-mix(in srgb, var(--color-error) 12%, var(--surface));
}
.task-review-tab__reviewers,
@@ -173,27 +258,52 @@
}
@media (max-width: 768px) {
.task-review-tab__row {
grid-template-columns: auto 1fr;
.task-review-tab {
gap: var(--space-md);
}
.task-review-tab__status {
grid-column: 2;
.task-review-tab__header {
flex-direction: column;
align-items: stretch;
gap: var(--space-md);
}
.task-review-tab__summary-wrap,
.task-review-tab__actions,
.task-review-tab__auto-merge-control {
width: 100%;
}
.task-review-tab__actions {
width: 100%;
justify-content: flex-start;
gap: var(--space-sm);
flex-wrap: wrap;
}
.task-review-tab__auto-merge-control {
.task-review-tab__actions .btn {
width: 100%;
min-inline-size: 0;
justify-content: center;
}
.task-review-tab__auto-merge-control .select {
width: 100%;
.task-review-tab__item,
.task-review-tab__body,
.task-review-tab__refresh-meta,
.task-review-tab__empty,
.task-review-tab__error {
min-width: 0;
}
.task-review-tab__item-header {
flex-direction: column;
align-items: stretch;
}
.task-review-tab__status,
.task-review-tab__decision {
max-width: 100%;
}
.task-review-tab__body {
padding: var(--space-sm);
}
.task-review-tab__refresh-meta {

View File

@@ -314,8 +314,10 @@ export function TaskReviewTab({
<div className="task-review-tab">
<div className="task-review-tab__header">
<div className="task-review-tab__summary-wrap">
<p className="task-review-tab__summary">{summaryText}</p>
{decisionLabel ? <span className={`task-review-tab__decision task-review-tab__decision--${decisionLabel}`}>{decisionLabel}</span> : null}
<div className="task-review-tab__summary-group">
<p className="task-review-tab__summary">{summaryText}</p>
{decisionLabel ? <span className={`task-review-tab__decision task-review-tab__decision--${decisionLabel}`}>{decisionLabel}</span> : null}
</div>
</div>
<div className="task-review-tab__actions">
<div className="task-review-tab__auto-merge-control">
@@ -373,16 +375,20 @@ export function TaskReviewTab({
<li key={item.id} className="task-review-tab__item card">
<div className="task-review-tab__item-inner">
<label htmlFor={checkboxId} className="task-review-tab__direct-item task-review-tab__direct-item--selectable">
<div className="task-review-tab__summary-wrap">
<input id={checkboxId} type="checkbox" checked={selected.includes(item.id)} onChange={() => toggleSelected(item.id)} />
<span className="task-review-tab__item-summary">{item.path ? `${item.path}: ` : ""}{item.summary}</span>
<div className="task-review-tab__item-header">
<div className="task-review-tab__item-selection">
<input id={checkboxId} type="checkbox" checked={selected.includes(item.id)} onChange={() => toggleSelected(item.id)} />
<span className="task-review-tab__item-summary">{item.path ? `${item.path}: ` : ""}{item.summary}</span>
</div>
<span className={`task-review-tab__status task-review-tab__status--${item.status}`}>{item.status}</span>
</div>
</label>
<div className="task-review-tab__meta">{formatTimestamp(item.createdAt)}</div>
{item.addressing ? (
<div className="task-review-tab__meta">Selected: {formatTimestamp(item.addressing.selectedAt)}{item.addressing.startedAt ? ` · Started: ${formatTimestamp(item.addressing.startedAt)}` : ""}{item.addressing.completedAt ? ` · Completed: ${formatTimestamp(item.addressing.completedAt)}` : ""}{item.addressing.error ? ` · Error: ${item.addressing.error}` : ""}</div>
) : null}
<div className="task-review-tab__item-meta-list">
<div className="task-review-tab__meta">{formatTimestamp(item.createdAt)}</div>
{item.addressing ? (
<div className="task-review-tab__meta">Selected: {formatTimestamp(item.addressing.selectedAt)}{item.addressing.startedAt ? ` · Started: ${formatTimestamp(item.addressing.startedAt)}` : ""}{item.addressing.completedAt ? ` · Completed: ${formatTimestamp(item.addressing.completedAt)}` : ""}{item.addressing.error ? ` · Error: ${item.addressing.error}` : ""}</div>
) : null}
</div>
{renderMarkdown ? (
<div className="task-review-tab__body markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>

View File

@@ -125,7 +125,7 @@ describe("TaskReviewTab", () => {
expect(await screen.findByText("Failed to load review data.")).toBeInTheDocument();
});
it("renders PR decision and status modifiers", async () => {
it("renders PR decision, status modifiers, and populated layout hooks", async () => {
const task = makeTask({
reviewState: {
source: "pull-request",
@@ -136,6 +136,8 @@ describe("TaskReviewTab", () => {
body: "Fix null handling",
author: { login: "reviewer" },
createdAt: new Date().toISOString(),
path: "src/parser.ts",
summary: "Parser guard is missing",
},
],
addressing: [{ itemId: "ri-1", status: "failed", selectedAt: new Date().toISOString() }],
@@ -143,9 +145,19 @@ describe("TaskReviewTab", () => {
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("CHANGES_REQUESTED");
expect(screen.getByText("failed").className).toContain("task-review-tab__status--failed");
expect(container.querySelector(".task-review-tab__header")).not.toBeNull();
expect(container.querySelector(".task-review-tab__summary-group")).not.toBeNull();
expect(container.querySelector(".task-review-tab__actions")).not.toBeNull();
expect(container.querySelector(".task-review-tab__refresh-meta")).not.toBeNull();
expect(container.querySelector(".task-review-tab__list")).not.toBeNull();
expect(container.querySelector(".task-review-tab__item-header")).not.toBeNull();
expect(container.querySelector(".task-review-tab__item-selection")).not.toBeNull();
expect(container.querySelector(".task-review-tab__item-meta-list")).not.toBeNull();
expect(container.querySelector(".task-review-tab__body")).not.toBeNull();
});
it("keeps review body outside the checkbox label and preserves selection on body clicks", async () => {
@@ -369,7 +381,7 @@ describe("TaskReviewTab", () => {
expect(addToast).toHaveBeenCalledWith("Review refreshed", "success");
});
it("renders reviewer-agent entries in direct mode", async () => {
it("renders reviewer-agent entries in direct mode with populated layout hooks", async () => {
const task = makeTask();
apiMocks.fetchTaskReview.mockResolvedValue({
reviewState: {
@@ -393,9 +405,11 @@ describe("TaskReviewTab", () => {
emptyMessage: null,
});
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("code review Step 2: REVISE")).toBeInTheDocument();
expect(screen.getAllByText("REVISE").length).toBeGreaterThan(0);
expect(container.querySelector(".task-review-tab__item-header")).not.toBeNull();
expect(container.querySelector(".task-review-tab__item-meta-list")).not.toBeNull();
});
it("renders all persisted addressing progress states from snapshots", async () => {
@@ -485,14 +499,20 @@ describe("TaskReviewTab", () => {
expect(screen.getByText(/Error: Patch failed/)).toBeInTheDocument();
});
it("keeps mobile actions wrapping contract and removes equal-width flex buttons", async () => {
it("keeps mobile actions wrapping contract, stacks header groups, and prevents body overflow regressions", async () => {
const css = await loadAllAppCss();
const mobileMediaStart = css.indexOf("@media (max-width: 768px)");
expect(mobileMediaStart).toBeGreaterThanOrEqual(0);
const mobileCss = css.slice(mobileMediaStart);
expect(mobileCss).toMatch(/\.task-review-tab__actions\s*\{[^}]*flex-wrap\s*:\s*wrap\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__header\s*\{[^}]*flex-direction\s*:\s*column\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__actions\s*\{[^}]*justify-content\s*:\s*flex-start\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__actions\s+\.btn\s*\{[^}]*width\s*:\s*100%\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__body\s*\{[^}]*padding\s*:\s*var\(--space-sm\)\s*;[^}]*\}/);
expect(mobileCss).not.toMatch(/\.task-review-tab__actions\s+\.btn\s*\{[^}]*flex\s*:\s*1\s*;[^}]*\}/);
expect(css).toMatch(/\.task-review-tab__body\s*\{[^}]*overflow-x\s*:\s*auto\s*;[^}]*overflow-wrap\s*:\s*anywhere\s*;[^}]*\}/);
expect(css).toMatch(/\.task-review-tab__item\s*\{[^}]*padding\s*:\s*var\(--card-padding\)\s*;[^}]*\}/);
});
it("shows create PR action when in-review without prInfo and auth is available", async () => {