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:
7
.changeset/fn-8599-task-card-size-badge.md
Normal file
7
.changeset/fn-8599-task-card-size-badge.md
Normal 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.
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user