From da919fddea27819426d70afe2d56a02c6e2b4c1b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 13 Jul 2026 00:01:42 -0700 Subject: [PATCH] FN-7928: align TaskCard header-actions controls on a shared centerline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes vertical baseline drift among the Send-back button, ⋯ menu button, and size badge inside .card-header-actions by normalizing their line-height, plus adds regression coverage. - Set line-height: 1 on .card-menu-btn and .card-send-back-btn so their inline-flex content no longer drifts off-center relative to the size badge - Add FNXC:TaskCardLayout comment documenting the FN-7928 requirement and its coexistence with prior FN-7889/FN-7862/FN-7837/FN-4351 header rules - Add a badge-wrap regression test asserting Send-back/menu/size controls share one optical centerline across in-progress, done, triage, no-menu, and no-size card states Files changed: packages/dashboard/app/components/TaskCard.css | 6 + packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx | 136 +++++++++++++++++++++ 2 files changed, 142 insertions(+) Fusion-Task-Id: FN-7928 Fusion-Task-Lineage: 1b4ad644-8495-4ea1-8687-0849d6a3319b Co-authored-by: Fusion (runfusion.ai) --- .../dashboard/app/components/TaskCard.css | 6 + .../__tests__/TaskCard.badge-wrap.test.tsx | 136 ++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index fcef0e2032..8e4c1be8df 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -1521,6 +1521,7 @@ short-landscape variant (FN-5751 lesson: never a desktop-only or portrait-only f width: 20px; height: 20px; padding: 0; + line-height: 1; background: transparent; border: none; border-radius: var(--radius-sm); @@ -1650,6 +1651,10 @@ too so future divergence doesn't have to rediscover the shared selector group. position: relative; } +/* +FNXC:TaskCardLayout 2026-07-12-00:00: +FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge to share one optical vertical center inside .card-header-actions. Normalize their line boxes while preserving FN-7889's cluster↔id transform nudge, FN-7862's flex-start header anchor, FN-7837's middle-badge wrap/size-chip-not-orphaned contract, and FN-4351's mobile no-min-height rule. +*/ .card-send-back-btn { display: inline-flex; align-items: center; @@ -1659,6 +1664,7 @@ too so future divergence doesn't have to rediscover the shared selector group. margin-left: var(--space-xs); font-size: 0.625rem; font-weight: 500; + line-height: 1; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); 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 ca69ebb5ce..4f0293c5a8 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx @@ -82,6 +82,55 @@ function expectSharedHeaderBaseline(container: HTMLElement) { expect(actionsStyles.flex).toBe("0 0 auto"); } +function expectHeaderActionsControlCenterline(container: HTMLElement, expected: { + sendBack?: boolean; + menu?: boolean; + size?: boolean; +}) { + const actions = container.querySelector(".card-header-actions") as HTMLElement; + expect(actions).toBeTruthy(); + expect(getComputedStyle(actions).alignItems).toBe("center"); + + const sendBack = actions.querySelector(".card-send-back-btn") as HTMLElement | null; + const menu = actions.querySelector(".card-menu-btn") as HTMLElement | null; + const sizeBadge = actions.querySelector(".card-size-badge") as HTMLElement | null; + + if (expected.sendBack) { + expect(sendBack).toBeTruthy(); + const sendBackStyles = getComputedStyle(sendBack!); + expect(sendBackStyles.display).toBe("inline-flex"); + expect(sendBackStyles.alignItems).toBe("center"); + expect(sendBackStyles.lineHeight).toBe("1"); + expect(sendBackStyles.minHeight).toBe(""); + } else { + expect(sendBack).toBeNull(); + } + + if (expected.menu) { + expect(menu).toBeTruthy(); + const menuStyles = getComputedStyle(menu!); + expect(menuStyles.display).toBe("flex"); + expect(menuStyles.alignItems).toBe("center"); + expect(menuStyles.justifyContent).toBe("center"); + expect(menuStyles.lineHeight).toBe("1"); + expect(menuStyles.minHeight).toBe(""); + } else { + expect(menu).toBeNull(); + } + + if (expected.size) { + expect(sizeBadge).toBeTruthy(); + const sizeStyles = getComputedStyle(sizeBadge!); + expect(sizeStyles.display).toBe("inline-flex"); + expect(sizeStyles.alignItems).toBe("center"); + expect(sizeStyles.lineHeight).toBe("1"); + expect(actions.contains(sizeBadge)).toBe(true); + expect(sizeBadge!.closest(".card-header-badges")).toBeNull(); + } else { + expect(sizeBadge).toBeNull(); + } +} + function makeTask(overrides: Partial = {}): Task { return { id: "FN-5162", @@ -270,6 +319,93 @@ describe("TaskCard badge wrapping (FN-5162)", () => { expectSharedHeaderBaseline(triageContainer); }); + it("keeps Send back, menu, and size controls on one header-actions centerline across card states", () => { + const { container: inProgressContainer } = render( + makeTask()} + />, + ); + + expectSharedHeaderBaseline(inProgressContainer); + expectHeaderActionsControlCenterline(inProgressContainer, { sendBack: true, menu: true, size: true }); + + const { container: doneContainer } = render( + makeTask()} + />, + ); + + expectSharedHeaderBaseline(doneContainer); + expectHeaderActionsControlCenterline(doneContainer, { sendBack: true, menu: true, size: true }); + + const { container: triageContainer } = render( + makeTask()} + onDeleteTask={async () => makeTask()} + />, + ); + + expectSharedHeaderBaseline(triageContainer); + expectHeaderActionsControlCenterline(triageContainer, { menu: true, size: true }); + + const { container: menuAbsentContainer } = render( + , + ); + + expectSharedHeaderBaseline(menuAbsentContainer); + expectHeaderActionsControlCenterline(menuAbsentContainer, { size: true }); + + const { container: sizeAbsentContainer } = render( + makeTask()} + />, + ); + + expectSharedHeaderBaseline(sizeAbsentContainer); + expectHeaderActionsControlCenterline(sizeAbsentContainer, { sendBack: true, menu: true }); + }); + it("keeps the centered-id nudge and mobile header rhythm tokenized with the badge-wrap contract", () => { const cardIdRule = loadedCss.match(/\.card-id\s*\{(?[^}]*)\}/)?.groups?.body ?? ""; const actionsRule = loadedCss.match(/\.card-header-actions\s*\{(?[^}]*)\}/)?.groups?.body ?? "";