FN-7933: align mobile task-card header controls to one centerline

Follow-up to FN-7928: fix vertical alignment of the Send-back/Actions trigger, menu button, and size badge in the mobile task-card header actions.

- Set line-height: 1 on .card-menu-btn, .card-size-badge, and .card-send-back-btn so their text/icon baselines match instead of drifting from default line-height.
- Add align-items: center to .card-header-actions at the mobile breakpoint so Send back, menu, and size badge share one optical vertical centerline.
- Add padding-block to .card-size-badge to keep the badge's rendered height consistent with the neighboring controls after the line-height fix.
- Add regression tests (TaskCard.badge-wrap.test.tsx) asserting the mobile header-actions rule set (min-height, align-items, gap) and per-control line-height/padding declarations, plus an awaiting-user-input coverage case exercising the send-back/menu/size-badge centerline together.

Files changed:
 packages/dashboard/app/components/TaskCard.css     | 16 ++++
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 92 ++++++++++++++++++++++
 2 files changed, 108 insertions(+)

Fusion-Task-Id: FN-7933
Fusion-Task-Lineage: e3d52b36-2446-4cdf-8faf-5c94b0f52786
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-13 00:53:48 -07:00
parent 1f9dcea4b6
commit cf58d7234d
2 changed files with 108 additions and 0 deletions

View File

@@ -1555,6 +1555,7 @@ short-landscape variant (FN-5751 lesson: never a desktop-only or portrait-only f
opacity: 1;
width: 28px;
height: 28px;
line-height: 1;
border-radius: var(--radius-md);
}
@@ -1822,11 +1823,21 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
min-height: var(--card-chip-height-mobile);
}
/*
FNXC:TaskCardLayout 2026-07-13-00:00:
FN-7933 follows FN-7928 by keeping the Send-back/Actions trigger, ⋯ menu button, and size badge on one optical vertical center inside the header actions at the mobile breakpoint. Preserve FN-7889's cluster↔id nudge, FN-7862's flex-start header anchor, FN-7837's badge-wrap/size-chip grouping, FN-7928's desktop centering, and FN-4351's no-min-height mobile rule for the send-back/menu buttons.
*/
.card-header-actions {
align-items: center;
gap: calc(var(--space-xs) / 2);
flex-shrink: 0;
}
.card-menu-btn,
.card-size-badge {
line-height: 1;
}
/* FN-4351/FN-3965: keep secondary actions visible on touch, but compact per WCAG 2.5.8 because the card tap surface is the primary target for opening task detail. */
.card-archive-btn,
.card-unarchive-btn,
@@ -1836,6 +1847,7 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
.card-send-back-btn {
opacity: 1;
line-height: 1;
}
.card-promote-action.card-send-back-btn {
@@ -1900,6 +1912,10 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);
}
.card-size-badge {
padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));
}
.card-stalled-review-reason {
font-size: 0.625rem;
-webkit-line-clamp: 3;

View File

@@ -82,6 +82,54 @@ function expectSharedHeaderBaseline(container: HTMLElement) {
expect(actionsStyles.flex).toBe("0 0 auto");
}
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));
}
expect(blocks.length).toBeGreaterThan(0);
return blocks;
}
function getCssRuleBodies(section: string, selectorFragment: string): string[] {
const bodies: string[] = [];
const pattern = /([^{}]+)\{([\s\S]*?)\}/g;
for (const match of section.matchAll(pattern)) {
if (match[1].includes(selectorFragment)) {
bodies.push(match[2]);
}
}
expect(bodies.length, `Expected CSS rule for ${selectorFragment}`).toBeGreaterThan(0);
return bodies;
}
function expectCssRuleToContain(section: string, selectorFragment: string, declaration: string): void {
const bodies = getCssRuleBodies(section, selectorFragment);
expect(bodies.some((body) => body.includes(declaration)), `${selectorFragment} should include ${declaration}`).toBe(true);
}
function expectCssRuleNotToContain(section: string, selectorFragment: string, declaration: string): void {
const bodies = getCssRuleBodies(section, selectorFragment);
for (const body of bodies) {
expect(body, `${selectorFragment} should not include ${declaration}`).not.toContain(declaration);
}
}
function expectHeaderActionsControlCenterline(container: HTMLElement, expected: {
sendBack?: boolean;
menu?: boolean;
@@ -404,21 +452,65 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expectSharedHeaderBaseline(sizeAbsentContainer);
expectHeaderActionsControlCenterline(sizeAbsentContainer, { sendBack: true, menu: true });
const { container: awaitingInputContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7933-AWAITING-INPUT",
column: "in-progress",
status: "awaiting-user-input" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
onOpenDetailWithTab={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
expect(awaitingInputContainer.querySelector(".card-answer-questions-btn")).toBeTruthy();
expectSharedHeaderBaseline(awaitingInputContainer);
expectHeaderActionsControlCenterline(awaitingInputContainer, { sendBack: true, menu: true, size: true });
});
it("keeps the centered-id nudge and mobile header rhythm tokenized with the badge-wrap contract", () => {
const cardHeaderRule = loadedCss.match(/\.card-header\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const cardIdRule = loadedCss.match(/\.card-id\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const actionsRule = loadedCss.match(/\.card-header-actions\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
expect(cardHeaderRule).toContain("align-items: flex-start;");
expect(cardIdRule).toContain("min-height: var(--card-chip-height);");
expect(cardIdRule).toContain("line-height: 1;");
expect(cardIdRule).toContain("transform: translateY(calc(var(--space-xs) / 4));");
expect(cardIdRule).not.toMatch(/translateY\(\d/);
expect(actionsRule).toContain("align-items: center;");
expect(actionsRule).toContain("transform: translateY(calc(var(--space-xs) / 4));");
expect(actionsRule).not.toMatch(/translateY\(\d/);
expect(loadedCss).toContain(".card-id,\n .card-header-badges,\n .card-header-actions");
expect(loadedCss).toContain("min-height: var(--card-chip-height-mobile);");
});
it("locks the mobile Send back, menu, and size controls to one header-actions centerline", () => {
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
const menuTouchSection = getCssBlocks(loadedCss, "max-height: 480px").join("\n");
expectCssRuleToContain(mobileSection, ".card-header-actions", "min-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "align-items: center;");
expectCssRuleToContain(mobileSection, ".card-header-actions", "gap: calc(var(--space-xs) / 2);");
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-menu-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "font-size: 0.5625rem;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);");
expectCssRuleToContain(mobileSection, ".card-size-badge", "padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));");
expectCssRuleNotToContain(mobileSection, ".card-send-back-btn", "min-height:");
expectCssRuleNotToContain(mobileSection, ".card-menu-btn", "min-height:");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "width: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "height: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "line-height: 1;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "width: 16px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "height: 16px;");
});
it.each([
".card-status-badge",
".card-priority-badge",