FN-8675: align task-card size badges
Align direct size chips with adjacent task-card header badges. - Center size badges on the shared header row across desktop and responsive layouts. - Add coverage for size badge alignment, dimensions, and responsive CSS rules. - Add a patch changeset for the dashboard alignment fix. Files changed: .changeset/fn-8675-size-badge-alignment.md | 7 +++ packages/dashboard/app/components/TaskCard.css | 8 +++ .../__tests__/TaskCard.badge-height.test.tsx | 68 ++++++++++++++++++++++ .../__tests__/TaskCard.badge-wrap.test.tsx | 10 ++++ 4 files changed, 93 insertions(+) Fusion-Task-Id: FN-8675 Fusion-Task-Lineage: bf2eefb8-dbc2-40dd-a009-800f883146a8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8675-size-badge-alignment.md
Normal file
7
.changeset/fn-8675-size-badge-alignment.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Align task-card size badges with neighboring header badges.
|
||||
category: fix
|
||||
dev: Keeps direct size chips in their first header row with token-derived vertical centering.
|
||||
@@ -548,11 +548,15 @@ FN-6774 removes the saturated stuck-task edge stripe from board cards. Keep the
|
||||
/*
|
||||
FNXC:TaskCardLayout 2026-08-01-04:48 (FN-8665):
|
||||
The size chip must derive its box from the shared header-chip padding, line-height, and border so it renders at the same height as status and metadata chips. As a direct child after .card-id, keep it anchored to the header's first row when the middle badge group wraps.
|
||||
|
||||
FNXC:TaskCardLayout 2026-08-01-06:46 (FN-8675):
|
||||
Every task-card header badge must share the first-row vertical centerline. The direct size chip keeps FN-8665's intrinsic badge geometry, so offset it downward by the token-derived space that centers status and metadata chips inside `.card-header-badges` rather than giving it a divergent fixed row height.
|
||||
*/
|
||||
.card-size-badge {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(calc((var(--space-xs) * 3) / 4));
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@@ -1689,10 +1693,14 @@ The mobile/short-landscape 28px ⋯ touch target must not expand .card-header-ac
|
||||
/*
|
||||
FNXC:TaskCardLayout 2026-08-01-04:48 (FN-8665):
|
||||
Narrow and short-landscape cards keep the size chip first-row anchored while its shared mobile text-chip rule supplies the same intrinsic height as neighboring badges.
|
||||
|
||||
FNXC:TaskCardLayout 2026-08-01-06:46 (FN-8675):
|
||||
Mobile and short-landscape header badges require the same centered first-row alignment as desktop. Preserve the direct size chip's compact intrinsic geometry and apply the token-derived row-center offset instead of changing its height.
|
||||
*/
|
||||
.card-size-badge {
|
||||
align-self: flex-start;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(calc((var(--space-xs) * 3) / 4));
|
||||
}
|
||||
|
||||
.card-menu-btn {
|
||||
|
||||
@@ -66,6 +66,27 @@ function mountCss() {
|
||||
return () => style.remove();
|
||||
}
|
||||
|
||||
function getCssBlocks(css: string, atRuleFragment: string): string[] {
|
||||
const re = /@media[^{}]*\{/g;
|
||||
const blocks: string[] = [];
|
||||
|
||||
for (const match of css.matchAll(re)) {
|
||||
if (!match[0].includes(atRuleFragment)) continue;
|
||||
const start = match.index! + match[0].length;
|
||||
let depth = 1;
|
||||
let i = start;
|
||||
while (i < css.length && depth > 0) {
|
||||
const ch = css[i];
|
||||
if (ch === "{") depth++;
|
||||
else if (ch === "}") depth--;
|
||||
i++;
|
||||
}
|
||||
blocks.push(css.slice(start, i - 1));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
describe("TaskCard badge heights (FN-4369)", () => {
|
||||
it("keeps triage planning, merging, and priority pills at identical dimensions", () => {
|
||||
const cleanupCss = mountCss();
|
||||
@@ -207,4 +228,51 @@ describe("TaskCard badge heights (FN-4369)", () => {
|
||||
|
||||
cleanupCss();
|
||||
});
|
||||
|
||||
it.each(["S", "M", "L"] as const)("keeps size %s on the centered header-badge row across responsive sections", (size) => {
|
||||
const cleanupCss = mountCss();
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
id: `FN-8675-${size}`,
|
||||
status: "planning" as Task["status"],
|
||||
size,
|
||||
priority: "urgent" as Task["priority"],
|
||||
executionMode: "fast",
|
||||
missionId: "M-8675",
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const sizeBadge = container.querySelector(".card-size-badge") as HTMLElement;
|
||||
const statusBadge = container.querySelector(".card-status-badge") as HTMLElement;
|
||||
const headerBadges = container.querySelector(".card-header-badges") as HTMLElement;
|
||||
expect(sizeBadge).toBeTruthy();
|
||||
expect(statusBadge).toBeTruthy();
|
||||
expect(headerBadges).toBeTruthy();
|
||||
expect(headerBadges.querySelectorAll(".card-status-badge, .card-priority-badge, .card-execution-mode-badge, .card-mission-badge").length).toBeGreaterThan(1);
|
||||
|
||||
/*
|
||||
* FNXC:TaskCardLayout 2026-08-01-06:46 (FN-8675):
|
||||
* Direct size chips cannot use `align-self: center` because a wrapping middle group would center
|
||||
* them over the whole header. Keep their intrinsic FN-8665 geometry and require the token-based
|
||||
* offset that matches the group's first-row centered chip position on desktop, mobile, and short landscape.
|
||||
*/
|
||||
const rowCenterOffset = "translateY(calc((var(--space-xs) * 3) / 4))";
|
||||
expect(getComputedStyle(sizeBadge).transform).toBe(rowCenterOffset);
|
||||
expect(getComputedStyle(statusBadge).height).toBe(getComputedStyle(sizeBadge).height);
|
||||
|
||||
const css = loadAllAppCss();
|
||||
const baseSizeRule = css.match(/\.card-size-badge\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
|
||||
expect(baseSizeRule).toContain(`transform: ${rowCenterOffset};`);
|
||||
for (const breakpoint of ["max-width: 768px", "max-height: 480px"]) {
|
||||
const sections = getCssBlocks(css, breakpoint);
|
||||
expect(sections.length).toBeGreaterThan(0);
|
||||
expect(sections.some((section) => /\.card-size-badge\s*\{[^}]*transform: translateY\(calc\(\(var\(--space-xs\) \* 3\) \/ 4\)\);/.test(section))).toBe(true);
|
||||
}
|
||||
|
||||
cleanupCss();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ vi.mock("../../hooks/useToast", () => ({
|
||||
const noop = () => {};
|
||||
const resolvedChipHeightPattern = /^(var\(--card-chip-height\)|22px)$/;
|
||||
const centeredIdNudgePattern = /^translateY\(calc\(var\(--space-xs\) \/ 4\)\)$/;
|
||||
const centeredSizeBadgeOffset = "transform: translateY(calc((var(--space-xs) * 3) / 4));";
|
||||
|
||||
function expectSharedHeaderBaseline(container: HTMLElement) {
|
||||
const header = container.querySelector(".card-header") as HTMLElement;
|
||||
@@ -196,6 +197,7 @@ function expectSizeBadgeAfterTaskId(container: HTMLElement, expected: boolean) {
|
||||
expect(sizeStyles.height).toBe("auto");
|
||||
expect(sizeStyles.minHeight).toBe("auto");
|
||||
expect(sizeStyles.maxHeight).toBe("none");
|
||||
expect(sizeStyles.transform).toBe("translateY(calc((var(--space-xs) * 3) / 4))");
|
||||
expect(sizeBadge!.parentElement).toBe(header);
|
||||
expect(cardId.nextElementSibling).toBe(sizeBadge);
|
||||
expect(actions?.contains(sizeBadge)).toBe(false);
|
||||
@@ -581,14 +583,22 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
|
||||
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
|
||||
const shortLandscapeSection = getCssBlocks(loadedCss, "max-height: 480px").join("\n");
|
||||
|
||||
/*
|
||||
* FNXC:TaskCardLayout 2026-08-01-06:46 (FN-8675):
|
||||
* The direct size chip retains intrinsic badge dimensions for FN-8665 parity, then uses this
|
||||
* token-derived offset to occupy the same first-row centerline as chips centered by the group.
|
||||
* Check every responsive section because jsdom does not evaluate media queries during render.
|
||||
*/
|
||||
expect(sizeBadgeRule).toContain("align-self: flex-start;");
|
||||
expect(sizeBadgeRule).toContain("box-sizing: border-box;");
|
||||
expect(sizeBadgeRule).toContain(centeredSizeBadgeOffset);
|
||||
expect(sizeBadgeRule).not.toContain("align-self: center;");
|
||||
for (const declaration of ["\n height:", "\n min-height:", "\n max-height:"]) {
|
||||
expectCssRuleNotToContain(loadedCss, ".card-size-badge", declaration);
|
||||
}
|
||||
for (const section of [mobileSection, shortLandscapeSection]) {
|
||||
expectCssRuleToContain(section, ".card-size-badge", "align-self: flex-start;");
|
||||
expectCssRuleToContain(section, ".card-size-badge", centeredSizeBadgeOffset);
|
||||
for (const declaration of ["\n height:", "\n min-height:", "\n max-height:"]) {
|
||||
expectCssRuleNotToContain(section, ".card-size-badge", declaration);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user