FN-8254: standardize task-card header badge heights

Standardize TaskCard header badge geometry across text and icon-only variants.

- Align planner oversight, fast-mode, mission, PR, and existing header chips with shared badge padding, borders, line height, and icon sizing.
- Cover populated desktop header chips and shared mobile chip declarations with regression tests.

Files changed:
 packages/dashboard/app/components/TaskCard.css     | 75 ++++++++++++++++------
 .../__tests__/TaskCard.badge-height.test.tsx       | 69 ++++++++++++++++++--
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 26 ++++++--
 3 files changed, 142 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-8254

Fusion-Task-Lineage: bd5e4cce-8641-47ac-8745-6465d5cbbc1a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 17:52:08 -07:00
parent d4de9ebf30
commit 12e0e3987b
3 changed files with 142 additions and 28 deletions

View File

@@ -471,22 +471,25 @@ warning badge, while generic and release-authorization holds retain the triage a
/*
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7592 replaces the overseer badge's uppercase text label with a small `Eye` icon so it
reads as a compact glyph rather than a wide pill. Size the badge to the icon (no min-width,
tight padding) and color it per `PlannerOverseerState` via the `data-planner-overseer-state`
attribute so operators can distinguish watching/steering/recovering/awaiting-confirmation at
reads as a compact glyph rather than a wide pill. Color it per `PlannerOverseerState` via the
`data-planner-overseer-state` attribute so operators can distinguish watching/steering/recovering/awaiting-confirmation at
a glance without reading text. Colors reuse existing semantic tokens: neutral/info for the
passive "watching" state, warning for the more active "steering"/"recovering" states, and the
triage token for "awaiting-confirmation" (a human-decision hold), matching the hue conventions
used elsewhere in this file (e.g. .card-oversight-badge--*, .card-status-badge--triage).
FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
The Eye inherits shared text-badge padding, border, and line height. Its token-sized glyph must
not enlarge that shared box, so icon-only and text header chips stay one height on desktop and mobile.
*/
.card-planner-overseer-state {
padding: calc(var(--space-xs) / 2);
line-height: 0;
box-sizing: border-box;
line-height: 1;
}
.card-planner-overseer-state svg {
width: 12px;
height: 12px;
width: 0.625rem;
height: 0.625rem;
}
.card-planner-overseer-state[data-planner-overseer-state="watching"] {
@@ -580,18 +583,27 @@ As a direct header child after .card-id, the size chip must match sibling header
color: var(--color-error-dark);
}
/*
FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
Fast-mode's icon-only chip shares the text-badge box geometry instead of a fixed square. Keep the
Zap at the badge text size so it remains centered without making the header row taller on desktop or mobile.
*/
.card-execution-mode-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
box-sizing: border-box;
font-size: 0.625rem;
font-weight: 600;
line-height: 1;
padding: calc(var(--space-xs) / 2) var(--space-sm);
border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
}
.card-execution-mode-badge svg {
width: 0.75rem;
height: 0.75rem;
width: 0.625rem;
height: 0.625rem;
}
.card-execution-mode-badge--fast {
@@ -823,17 +835,25 @@ falling through to an unstyled transparent shell.
box-shadow: var(--shadow-md);
}
/* Mission badge: clickable link to mission from task card */
/*
FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
The clickable mission chip uses the shared text-badge box geometry, including its transparent
border. This keeps its label and Target glyph on the same header baseline as status and icon-only chips
at desktop and mobile breakpoints.
*/
.card-mission-badge {
display: inline-flex;
align-items: center;
gap: 3px;
box-sizing: border-box;
gap: var(--space-xs);
font-size: 0.625rem;
font-weight: 600;
line-height: 1;
font-family: var(--font-mono);
color: var(--badge-mission-text);
background: var(--badge-mission-bg);
padding: 2px 6px;
padding: calc(var(--space-xs) / 2) var(--space-sm);
border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-pill);
cursor: pointer;
transition: color var(--transition-fast), background var(--transition-fast);
@@ -843,6 +863,11 @@ falling through to an unstyled transparent shell.
white-space: nowrap;
}
.card-mission-badge svg {
width: 0.625rem;
height: 0.625rem;
}
.card-mission-badge:hover {
color: var(--badge-mission-text-hover);
background: var(--badge-mission-bg-hover);
@@ -1931,19 +1956,31 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu
row-gap: var(--space-xs);
}
/* Card: smaller status badges for 280px width */
/*
FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
Every header chip uses this mobile text-badge scale. The former size-only block padding made
its box diverge; text, icon-only, PR, oversight, and mission chips must remain one height.
*/
.card-status-badge,
.card-priority-badge,
.card-size-badge,
/* FNXC:PlannerOversight 2026-07-04-00:00: mobile-scale the FN-7516 oversight
badge alongside the other .card-meta-badges chips at narrow widths. */
.card-no-commits-expected-badge,
.card-planner-overseer-state,
.card-execution-mode-badge,
.card-pr-node-badge,
.card-mission-badge,
.card-oversight-badge {
font-size: 0.5625rem;
line-height: 1;
padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);
border: var(--btn-border-width) solid transparent;
}
.card-size-badge {
padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));
.card-planner-overseer-state svg,
.card-execution-mode-badge svg,
.card-mission-badge svg {
width: 0.5625rem;
height: 0.5625rem;
}
.card-stalled-review-reason {

View File

@@ -13,13 +13,14 @@ vi.mock("lucide-react", () => ({
Layers: () => null,
ChevronDown: () => null,
Folder: () => null,
GitPullRequest: () => null,
GitPullRequest: () => <svg />,
CircleDot: () => null,
Target: () => null,
Target: () => <svg />,
Bot: () => null,
Trash2: () => null,
RotateCw: () => null,
Zap: () => null,
Zap: () => <svg />,
Eye: () => <svg />,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-down" style={style} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-flag" style={style} />,
@@ -66,11 +67,11 @@ function mountCss() {
}
describe("TaskCard badge heights (FN-4369)", () => {
it("keeps planning, merging, and priority pills at identical dimensions", () => {
it("keeps triage planning, merging, and priority pills at identical dimensions", () => {
const cleanupCss = mountCss();
const planning = render(
<TaskCard task={makeTask({ id: "FN-100", column: "in-progress", status: "planning" as Task["status"] })} onOpenDetail={noop} addToast={noop} />,
<TaskCard task={makeTask({ id: "FN-100", column: "triage", status: "planning" as Task["status"] })} onOpenDetail={noop} addToast={noop} />,
).container.querySelector(".card-status-badge");
const merging = render(
@@ -110,4 +111,62 @@ describe("TaskCard badge heights (FN-4369)", () => {
cleanupCss();
});
it("keeps every populated header chip on the shared text-badge geometry", () => {
const cleanupCss = mountCss();
const { container } = render(
<TaskCard
task={makeTask({
id: "FN-8254",
status: "running" as Task["status"],
size: "M",
priority: "urgent" as Task["priority"],
executionMode: "fast",
missionId: "M-8254",
plannerOversightLevel: "autonomous",
plannerOverseerState: {
state: "watching",
oversightLevel: "autonomous",
watchedStage: "executor",
signal: "progressing",
attemptCount: 0,
attemptLimit: 3,
pendingConfirmation: false,
observedAt: 1700000000000,
},
})}
onOpenDetail={noop}
addToast={noop}
onOpenPullRequest={noop}
prNode={{ id: "pr-8254", state: "open", prNumber: 8254 }}
/>,
);
const baseline = container.querySelector(".card-status-badge");
const chips = [
".card-planner-overseer-state",
".card-execution-mode-badge",
".card-pr-node-badge",
".card-mission-badge",
".card-size-badge",
".card-oversight-badge",
].map((selector) => container.querySelector(selector));
expect(baseline).toBeTruthy();
chips.forEach((chip) => expect(chip).toBeTruthy());
const baselineStyles = getComputedStyle(baseline!);
for (const chip of chips) {
const styles = getComputedStyle(chip!);
expect(styles.height).toBe(baselineStyles.height);
expect(styles.paddingTop).toBe(baselineStyles.paddingTop);
expect(styles.paddingBottom).toBe(baselineStyles.paddingBottom);
expect(styles.borderTopWidth).toBe(baselineStyles.borderTopWidth);
expect(styles.borderBottomWidth).toBe(baselineStyles.borderBottomWidth);
expect(styles.lineHeight).toBe(baselineStyles.lineHeight);
expect(styles.minHeight).toBe(baselineStyles.minHeight);
}
cleanupCss();
});
});

View File

@@ -543,10 +543,28 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "transform: translateY(calc(var(--space-xs) / -4));");
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));");
/*
* FNXC:TaskCardLayout 2026-07-17-15:00 (FN-8254):
* jsdom does not apply the mobile media query. Inspect its shared declaration directly so
* text, icon-only, size, PR, mission, and oversight chips cannot regain divergent box geometry.
*/
const mobileHeaderChipSelectors = [
".card-status-badge",
".card-priority-badge",
".card-size-badge",
".card-planner-overseer-state",
".card-execution-mode-badge",
".card-pr-node-badge",
".card-mission-badge",
".card-oversight-badge",
];
for (const selector of mobileHeaderChipSelectors) {
expectCssRuleToContain(mobileSection, selector, "font-size: 0.5625rem;");
expectCssRuleToContain(mobileSection, selector, "line-height: 1;");
expectCssRuleToContain(mobileSection, selector, "padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);");
expectCssRuleToContain(mobileSection, selector, "border: var(--btn-border-width) solid transparent;");
expectCssRuleNotToContain(mobileSection, selector, "padding-block:");
}
expectCssRuleNotToContain(mobileSection, ".card-send-back-btn", "min-height:");
expectCssRuleNotToContain(mobileSection, ".card-menu-btn", "min-height:");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "width: 28px;");