FN-8599: fix task card size badge alignment

Keep the size badge aligned with the first task-card header row when status badges wrap.

- Anchor desktop and mobile size chips to the header start with fixed tokenized heights
- Cover size-badge geometry and paused/reviewing badge combinations
- Add a patch changeset for the task-card alignment fix

Files changed:
 .changeset/fn-8599-task-card-size-badge.md         |  7 +++
 packages/dashboard/app/components/TaskCard.css     | 25 +++++++--
 .../__tests__/TaskCard.badge-height.test.tsx       | 13 ++++-
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 62 +++++++++++++++++++--
 4 files changed, 98 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8599

Fusion-Task-Lineage: 062716b6-58ca-4f5b-8741-50856e933192

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-26 08:42:46 -07:00
parent af897d9e3c
commit 9afd88dd9b
4 changed files with 98 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix task card size badge alignment when a card shows two status badges.
category: fix
dev: TaskCard.css anchors .card-size-badge to the first chip-height header row instead of centering over a wrapped header.

View File

@@ -547,13 +547,16 @@ FN-6774 removes the saturated stuck-task edge stripe from board cards. Keep the
} }
/* /*
FNXC:TaskCardLayout 2026-07-17-14:00 (FN-8253): FNXC:TaskCardLayout 2026-07-26-08:29 (FN-8599):
As a direct header child after .card-id, the size chip must match sibling header-badge height rather than use a bespoke chip-height minimum. Center it on the header row to preserve its baseline alignment with the id and trailing actions. Its existing color coding remains unchanged. As a direct header child after .card-id, the size chip must stay in the first chip-height row. Whole-header centering misaligned it when the middle badge group wrapped, so lock its tokenized height and anchor it at the header start while the shared badge rule centers its text.
*/ */
.card-size-badge { .card-size-badge {
align-self: center; align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
height: var(--card-chip-height);
min-height: var(--card-chip-height);
max-height: var(--card-chip-height);
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
@@ -1672,6 +1675,18 @@ FNXC:TaskCardLayout 2026-07-13-00:59:
The mobile/short-landscape 28px ⋯ touch target must not expand .card-header-actions' flex line. Without a negative vertical margin (same cancel pattern as .card-edit-btn/.card-delete-btn), align-items:center sinks the Actions/Send-back trigger below the mono task-id baseline on mobile. Keep the 28px hit box; cancel only the layout contribution so trailing actions stay vertically aligned with the task id (follow-up to FN-7933's within-cluster centerline). The mobile/short-landscape 28px ⋯ touch target must not expand .card-header-actions' flex line. Without a negative vertical margin (same cancel pattern as .card-edit-btn/.card-delete-btn), align-items:center sinks the Actions/Send-back trigger below the mono task-id baseline on mobile. Keep the 28px hit box; cancel only the layout contribution so trailing actions stay vertically aligned with the task id (follow-up to FN-7933's within-cluster centerline).
*/ */
@media (max-width: 768px), (max-height: 480px) { @media (max-width: 768px), (max-height: 480px) {
/*
FNXC:TaskCardLayout 2026-07-26-08:29 (FN-8599):
Narrow and short-landscape cards use the mobile chip rhythm. Keep the size chip first-row anchored instead of centering it over wrapped header badges.
*/
.card-size-badge {
align-self: flex-start;
box-sizing: border-box;
height: var(--card-chip-height-mobile);
min-height: var(--card-chip-height-mobile);
max-height: var(--card-chip-height-mobile);
}
.card-menu-btn { .card-menu-btn {
opacity: 1; opacity: 1;
width: 28px; width: 28px;
@@ -1894,8 +1909,8 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu
} }
/* /*
FNXC:TaskCardLayout 2026-07-17-14:00 (FN-8253): FNXC:TaskCardLayout 2026-07-26-08:29 (FN-8599):
At narrow and short-landscape breakpoints, the id, wrapping badge group, and trailing actions retain the mobile chip-height row. The direct id-adjacent size chip instead derives its height from shared badge geometry so it matches sibling header badges while its centered alignment preserves the non-wrapping header baseline. At narrow breakpoints, the id, wrapping badge group, and trailing actions retain the mobile chip-height row. The direct id-adjacent size chip has its own first-row anchor in the shared narrow/short-landscape block so wrapping cannot center it over the whole header.
*/ */
.card-id, .card-id,
.card-header-badges, .card-header-badges,

View File

@@ -148,12 +148,13 @@ describe("TaskCard badge heights (FN-4369)", () => {
".card-execution-mode-badge", ".card-execution-mode-badge",
".card-pr-node-badge", ".card-pr-node-badge",
".card-mission-badge", ".card-mission-badge",
".card-size-badge",
".card-oversight-badge", ".card-oversight-badge",
].map((selector) => container.querySelector(selector)); ].map((selector) => container.querySelector(selector));
const sizeBadge = container.querySelector(".card-size-badge");
expect(baseline).toBeTruthy(); expect(baseline).toBeTruthy();
chips.forEach((chip) => expect(chip).toBeTruthy()); chips.forEach((chip) => expect(chip).toBeTruthy());
expect(sizeBadge).toBeTruthy();
const baselineStyles = getComputedStyle(baseline!); const baselineStyles = getComputedStyle(baseline!);
for (const chip of chips) { for (const chip of chips) {
@@ -167,6 +168,16 @@ describe("TaskCard badge heights (FN-4369)", () => {
expect(styles.minHeight).toBe(baselineStyles.minHeight); expect(styles.minHeight).toBe(baselineStyles.minHeight);
} }
const sizeStyles = getComputedStyle(sizeBadge!);
expect(sizeStyles.height).toBe("var(--card-chip-height)");
expect(sizeStyles.minHeight).toBe("var(--card-chip-height)");
expect(sizeStyles.maxHeight).toBe("var(--card-chip-height)");
expect(sizeStyles.paddingTop).toBe(baselineStyles.paddingTop);
expect(sizeStyles.paddingBottom).toBe(baselineStyles.paddingBottom);
expect(sizeStyles.borderTopWidth).toBe(baselineStyles.borderTopWidth);
expect(sizeStyles.borderBottomWidth).toBe(baselineStyles.borderBottomWidth);
expect(sizeStyles.lineHeight).toBe(baselineStyles.lineHeight);
cleanupCss(); cleanupCss();
}); });
}); });

View File

@@ -184,7 +184,11 @@ function expectSizeBadgeAfterTaskId(container: HTMLElement, expected: boolean) {
const sizeStyles = getComputedStyle(sizeBadge!); const sizeStyles = getComputedStyle(sizeBadge!);
expect(sizeStyles.display).toBe("inline-flex"); expect(sizeStyles.display).toBe("inline-flex");
expect(sizeStyles.alignItems).toBe("center"); expect(sizeStyles.alignItems).toBe("center");
expect(sizeStyles.alignSelf).toBe("flex-start");
expect(sizeStyles.lineHeight).toBe("1"); expect(sizeStyles.lineHeight).toBe("1");
expect(sizeStyles.height).toMatch(resolvedChipHeightPattern);
expect(sizeStyles.minHeight).toMatch(resolvedChipHeightPattern);
expect(sizeStyles.maxHeight).toMatch(resolvedChipHeightPattern);
expect(sizeBadge!.parentElement).toBe(header); expect(sizeBadge!.parentElement).toBe(header);
expect(cardId.nextElementSibling).toBe(sizeBadge); expect(cardId.nextElementSibling).toBe(sizeBadge);
expect(actions?.contains(sizeBadge)).toBe(false); expect(actions?.contains(sizeBadge)).toBe(false);
@@ -519,11 +523,63 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expect(loadedCss).toContain("min-height: var(--card-chip-height-mobile);"); expect(loadedCss).toContain("min-height: var(--card-chip-height-mobile);");
}); });
it("derives the size badge height from shared header-badge geometry", () => { it("anchors the size badge to the first header chip row across desktop and mobile", () => {
const sizeBadgeRule = loadedCss.match(/\.card-size-badge\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? ""; const sizeBadgeRule = loadedCss.match(/\.card-size-badge\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
const shortLandscapeSection = getCssBlocks(loadedCss, "max-height: 480px").join("\n");
expect(sizeBadgeRule).toContain("align-self: center;"); expect(sizeBadgeRule).toContain("align-self: flex-start;");
expect(sizeBadgeRule).not.toContain("min-height:"); expect(sizeBadgeRule).toContain("box-sizing: border-box;");
expect(sizeBadgeRule).toContain("height: var(--card-chip-height);");
expect(sizeBadgeRule).toContain("min-height: var(--card-chip-height);");
expect(sizeBadgeRule).toContain("max-height: var(--card-chip-height);");
expect(sizeBadgeRule).not.toContain("align-self: center;");
for (const section of [mobileSection, shortLandscapeSection]) {
expectCssRuleToContain(section, ".card-size-badge", "align-self: flex-start;");
expectCssRuleToContain(section, ".card-size-badge", "height: var(--card-chip-height-mobile);");
expectCssRuleToContain(section, ".card-size-badge", "min-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(section, ".card-size-badge", "max-height: var(--card-chip-height-mobile);");
}
});
it("keeps the size chip beside the id when paused and reviewing badges fill the wrapping group", () => {
const { container: pausedContainer } = render(
<TaskCard
task={makeTask({
id: "FN-8599-PAUSED",
status: "planning" as Task["status"],
size: "M",
paused: true,
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const { container: reviewingContainer } = render(
<TaskCard
task={makeTask({
id: "FN-8599-REVIEWING",
column: "triage",
status: "planning" as Task["status"],
size: "M",
enabledWorkflowSteps: ["plan-review"],
workflowStepResults: [{
workflowStepId: "plan-review",
workflowStepName: "Plan Review",
status: "pending",
startedAt: "2026-07-26T08:29:00.000Z",
}],
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(pausedContainer.querySelectorAll(".card-status-badge")).toHaveLength(1);
expect(reviewingContainer.querySelectorAll(".card-status-badge")).toHaveLength(2);
expect(reviewingContainer.querySelector('[data-testid="card-reviewing-FN-8599-REVIEWING"]')).toHaveTextContent("Reviewing");
expectSizeBadgeAfterTaskId(pausedContainer, true);
expectSizeBadgeAfterTaskId(reviewingContainer, true);
}); });
it("locks the mobile three-dot menu, size, and Promote controls to the card rhythm", () => { it("locks the mobile three-dot menu, size, and Promote controls to the card rhythm", () => {