From 3b680cce045bde3bfc50600037b4e991878abd56 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 1 Aug 2026 00:10:04 -0700 Subject: [PATCH] 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) --- .changeset/fn-8675-size-badge-alignment.md | 7 ++ .../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(+) create mode 100644 .changeset/fn-8675-size-badge-alignment.md diff --git a/.changeset/fn-8675-size-badge-alignment.md b/.changeset/fn-8675-size-badge-alignment.md new file mode 100644 index 0000000000..2e9c411cdf --- /dev/null +++ b/.changeset/fn-8675-size-badge-alignment.md @@ -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. diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index ae56f40173..fc7be8ecfc 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -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 { diff --git a/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx index 4256a82fd1..73cb650591 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-height.test.tsx @@ -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( + , + ); + + 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*\{(?[^}]*)\}/)?.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(); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx index 85ab865f63..11cc3e7eca 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx @@ -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); }