FN-7928: align TaskCard header-actions controls on a shared centerline

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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-13 00:01:42 -07:00
parent 967f3dd900
commit da919fddea
2 changed files with 142 additions and 0 deletions

View File

@@ -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);

View File

@@ -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> = {}): 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(
<TaskCard
task={makeTask({
id: "FN-7928-IN-PROGRESS",
column: "in-progress",
status: "running" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(inProgressContainer);
expectHeaderActionsControlCenterline(inProgressContainer, { sendBack: true, menu: true, size: true });
const { container: doneContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-DONE",
column: "done",
status: "done" as Task["status"],
size: "S",
})}
onOpenDetail={noop}
addToast={noop}
onArchiveTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(doneContainer);
expectHeaderActionsControlCenterline(doneContainer, { sendBack: true, menu: true, size: true });
const { container: triageContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-TRIAGE",
column: "triage",
status: undefined,
size: "L",
})}
onOpenDetail={noop}
addToast={noop}
onUpdateTask={async () => makeTask()}
onDeleteTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(triageContainer);
expectHeaderActionsControlCenterline(triageContainer, { menu: true, size: true });
const { container: menuAbsentContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-NO-MENU",
column: "todo",
status: "pending" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expectSharedHeaderBaseline(menuAbsentContainer);
expectHeaderActionsControlCenterline(menuAbsentContainer, { size: true });
const { container: sizeAbsentContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-NO-SIZE",
column: "in-progress",
status: "running" as Task["status"],
size: undefined,
})}
onOpenDetail={noop}
addToast={noop}
onMoveTask={async () => 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*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const actionsRule = loadedCss.match(/\.card-header-actions\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";