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