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:
@@ -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);
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
Reference in New Issue
Block a user