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 ?? "";