fix: align TaskCard right header cluster with task-id baseline

Lock .card-id and .card-header-actions to the chip height so the mobile
28px ⋯ touch target cannot stretch the row and sink Actions/size below
FN-####. Cancel residual menu layout growth and extend badge-wrap
regression coverage for the shared locked-row contract.
This commit is contained in:
gsxdsm
2026-07-13 01:03:06 -07:00
parent cf58d7234d
commit 0b60fe9548
2 changed files with 66 additions and 2 deletions

View File

@@ -134,12 +134,18 @@ FN-7862 requires the task id, the first badge row, and the right-side controls t
/*
FNXC:TaskCardLayout 2026-07-12-00:00:
FN-7871 requires the mono task id to read optically centered with the first-row header badges and right-side icons/chips. Keep the outer FN-7862 flex-start first-row anchor and FN-7837 middle-badge wrapping intact; use a token-derived visual nudge on the id itself because its line-height: 1 glyph box sits slightly high inside the shared chip-height row.
FNXC:TaskCardLayout 2026-07-13-01:01:
Lock the id to the same chip-height row as .card-header-actions so the whole right cluster (Actions/⋯/size) shares one optical baseline with FN-#### on desktop and mobile.
*/
.card-id {
display: inline-flex;
align-items: center;
flex-shrink: 0;
box-sizing: border-box;
height: var(--card-chip-height);
min-height: var(--card-chip-height);
max-height: var(--card-chip-height);
line-height: 1;
transform: translateY(calc(var(--space-xs) / 4));
font-size: 0.6875rem;
@@ -1436,6 +1442,9 @@ executing. These map 1:1 to the unified progress status so the dot color encodes
/*
FNXC:TaskCardLayout 2026-07-12-00:00:
FN-7889 requires the right-side size/menu icons and action chips to share the mono task id's optical centerline. Mirror FN-7871's token-derived id nudge on the whole right cluster while preserving FN-7862's flex-start first-row anchor and FN-7837's middle-badge wrap/size-chip-not-orphaned contract.
FNXC:TaskCardLayout 2026-07-13-01:00:
The whole right cluster (Actions/Send-back, ⋯ menu, size badge) must share the task-id first-row baseline. Lock the cluster to the chip height so a larger touch-target child (28px ⋯) cannot grow the flex line and sink Actions/size below the id; overflow stays visible so the hit box can extend outside the locked row.
*/
.card-header-actions {
display: flex;
@@ -1443,8 +1452,12 @@ FN-7889 requires the right-side size/menu icons and action chips to share the mo
align-self: flex-start;
flex: 0 0 auto;
gap: var(--space-xs);
box-sizing: border-box;
height: var(--card-chip-height);
min-height: var(--card-chip-height);
max-height: var(--card-chip-height);
margin-left: auto;
overflow: visible;
transform: translateY(calc(var(--space-xs) / 4));
}
@@ -1548,13 +1561,20 @@ short-landscape variant (FN-5751 lesson: never a desktop-only or portrait-only f
outline-offset: 1px;
}
/* FNXC:TaskCardMenu 2026-07-10-12:00: mobile has no hover — the ⋯ affordance must be always visible
with a comfortable touch target. Includes short-landscape phones per the project mobile MQ. */
/*
FNXC:TaskCardMenu 2026-07-10-12:00:
Mobile has no hover — the ⋯ affordance must be always visible with a comfortable touch target. Includes short-landscape phones per the project mobile MQ.
FNXC:TaskCardLayout 2026-07-13-00:59:
The mobile/short-landscape 28px ⋯ touch target must not expand .card-header-actions' flex line. Without a negative vertical margin (same cancel pattern as .card-edit-btn/.card-delete-btn), align-items:center sinks the Actions/Send-back trigger and size badge below the mono task-id baseline on mobile. Keep the 28px hit box; cancel only the layout contribution so the right cluster stays vertically aligned with the task id (follow-up to FN-7933's within-cluster centerline).
*/
@media (max-width: 768px), (max-height: 480px) {
.card-menu-btn {
opacity: 1;
width: 28px;
height: 28px;
/* Match edit/delete: cancel 28px layout growth while preserving the larger tap target. */
margin: -6px 0;
line-height: 1;
border-radius: var(--radius-md);
}
@@ -1826,11 +1846,25 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
/*
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.
FNXC:TaskCardLayout 2026-07-13-01:01:
On mobile the entire right row (Actions/Send-back + ⋯ + size) must sit on the same first-row baseline as the task id. Lock the cluster (and the mono id) to the mobile chip height so the 28px ⋯ touch target cannot stretch the row and drop the whole right cluster below FN-#### / DONE. overflow:visible keeps the larger hit boxes paintable outside the locked row.
*/
.card-id {
height: var(--card-chip-height-mobile);
min-height: var(--card-chip-height-mobile);
max-height: var(--card-chip-height-mobile);
box-sizing: border-box;
}
.card-header-actions {
align-items: center;
gap: calc(var(--space-xs) / 2);
flex-shrink: 0;
height: var(--card-chip-height-mobile);
min-height: var(--card-chip-height-mobile);
max-height: var(--card-chip-height-mobile);
overflow: visible;
}
.card-menu-btn,
@@ -1838,6 +1872,13 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
line-height: 1;
}
/* Keep Actions/Send-back on the locked row centerline (no extra vertical box growth). */
.card-send-back {
display: inline-flex;
align-items: center;
height: 100%;
}
/* 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,
@@ -1848,6 +1889,8 @@ FN-7928 requires the Send-back/Actions trigger, ⋯ menu button, and size badge
.card-send-back-btn {
opacity: 1;
line-height: 1;
margin-top: 0;
margin-bottom: 0;
}
.card-promote-action.card-send-back-btn {

View File

@@ -73,11 +73,17 @@ function expectSharedHeaderBaseline(container: HTMLElement) {
expect(idStyles.alignItems).toBe("center");
expect(idStyles.lineHeight).toBe("1");
expect(idStyles.minHeight).toMatch(resolvedChipHeightPattern);
expect(idStyles.height).toMatch(resolvedChipHeightPattern);
expect(idStyles.maxHeight).toMatch(resolvedChipHeightPattern);
expect(idStyles.transform).toMatch(centeredIdNudgePattern);
expect(actionsStyles.display).toBe("flex");
expect(actionsStyles.alignItems).toBe("center");
expect(actionsStyles.transform).toBe(idStyles.transform);
expect(actionsStyles.minHeight).toMatch(resolvedChipHeightPattern);
// Locked chip-height row so a taller ⋯ touch target cannot sink the right cluster below the task id.
expect(actionsStyles.height).toMatch(resolvedChipHeightPattern);
expect(actionsStyles.maxHeight).toMatch(resolvedChipHeightPattern);
expect(actionsStyles.overflow).toBe("visible");
expect(actionsStyles.marginLeft).toBe("auto");
expect(actionsStyles.flex).toBe("0 0 auto");
}
@@ -479,10 +485,15 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
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("height: var(--card-chip-height);");
expect(cardIdRule).toContain("max-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("height: var(--card-chip-height);");
expect(actionsRule).toContain("max-height: var(--card-chip-height);");
expect(actionsRule).toContain("overflow: visible;");
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");
@@ -494,8 +505,16 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
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", "height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "max-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "overflow: visible;");
expectCssRuleToContain(mobileSection, ".card-header-actions", "align-items: center;");
expectCssRuleToContain(mobileSection, ".card-header-actions", "gap: calc(var(--space-xs) / 2);");
// Task id and right cluster share the same locked mobile chip row so Actions/⋯/size sit on the FN-#### baseline.
expectCssRuleToContain(mobileSection, ".card-id", "height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-id", "max-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-send-back", "height: 100%;");
expectCssRuleToContain(mobileSection, ".card-send-back", "align-items: center;");
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;");
@@ -507,6 +526,8 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "width: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "height: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "line-height: 1;");
// Negative vertical margin cancels residual 28px layout contribution (same pattern as .card-edit-btn/.card-delete-btn).
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "margin: -6px 0;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "width: 16px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "height: 16px;");
});