FN-8667: align fast-mode badges with task status

Keep fast-mode metadata badges in the task-card header's shared wrapping context.

- Make the meta badge wrapper layout-transparent so fast, priority, and oversight badges wrap independently with status.
- Prevent fast and priority chips from shrinking and update desktop and mobile layout coverage.
- Add a patch changeset for aligned task-card badges.

Files changed:
 .changeset/task-card-fast-mode-badge-wrap.md       |  7 ++++
 packages/dashboard/app/components/TaskCard.css     | 16 ++++----
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 46 ++++++++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     |  6 +--
 4 files changed, 64 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-8667

Fusion-Task-Lineage: 34bdb410-8d8a-4b1f-8bc4-105ae324874e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-31 22:51:07 -07:00
parent ee77a8d3fa
commit f5a776d923
4 changed files with 64 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Keep fast-mode task-card badges aligned with status badges when space allows.
category: fix
dev: The task-card meta badge wrapper is layout-transparent so nested chips share the header wrap context.

View File

@@ -125,13 +125,12 @@ The size chip is a direct header child immediately after the task id, before thi
min-height: var(--card-chip-height); min-height: var(--card-chip-height);
} }
/*
FNXC:TaskCardLayout 2026-08-01-05:38:
The fast-mode badge must share the header line with status, priority, and oversight badges whenever space remains. Keep this nested semantic wrapper layout-transparent so its meta badges join `.card-header-badges` directly instead of wrapping as one atomic group.
*/
.card-meta-badges { .card-meta-badges {
display: flex; display: contents;
align-items: center;
flex-wrap: wrap;
gap: var(--space-xs);
row-gap: var(--space-xs);
min-width: 0;
} }
/* /*
@@ -577,6 +576,7 @@ The size chip must derive its box from the shared header-chip padding, line-heig
.card-priority-badge { .card-priority-badge {
/* FNXC:PriorityIconOnlyBadge 2026-07-12-00:00: FN-7867 removes visible priority text from board cards, so these shared spacing declarations no longer add card text width; keep them to preserve the Task Detail priority chip/select geometry that reuses the same badge class. */ /* FNXC:PriorityIconOnlyBadge 2026-07-12-00:00: FN-7867 removes visible priority text from board cards, so these shared spacing declarations no longer add card text width; keep them to preserve the Task Detail priority chip/select geometry that reuses the same badge class. */
flex-shrink: 0;
gap: var(--space-xs); gap: var(--space-xs);
letter-spacing: 0.4px; letter-spacing: 0.4px;
} }
@@ -604,6 +604,7 @@ Zap at the badge text size so it remains centered without making the header row
.card-execution-mode-badge { .card-execution-mode-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex-shrink: 0;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
font-size: 0.625rem; font-size: 0.625rem;
@@ -2026,8 +2027,7 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu
} }
.card-header-badges, .card-header-badges {
.card-meta-badges {
gap: calc(var(--space-xs) / 2); gap: calc(var(--space-xs) / 2);
row-gap: var(--space-xs); row-gap: var(--space-xs);
} }

View File

@@ -282,6 +282,52 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expect(container.querySelector(".card-header-actions")).toBeNull(); expect(container.querySelector(".card-header-actions")).toBeNull();
}); });
it.each([
["fast alone", { priority: "normal", plannerOversightLevel: "off" }],
["fast with priority", { priority: "urgent", plannerOversightLevel: "off" }],
["fast with oversight", { priority: "normal", plannerOversightLevel: "steer" }],
["fast with priority and oversight", { priority: "urgent", plannerOversightLevel: "steer" }],
] as const)("keeps %s beside a queued-to-plan status chip in the shared wrap context", (_name, meta) => {
const { container: queuedContainer } = render(
<TaskCard
task={makeTask({
column: "todo",
status: undefined,
steps: [],
executionMode: "fast",
...meta,
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const headerBadges = queuedContainer.querySelector(".card-header-badges") as HTMLElement;
const statusBadge = queuedContainer.querySelector(".card-status-badge") as HTMLElement;
const metaBadges = queuedContainer.querySelector('[data-testid="card-meta-badges"]') as HTMLElement;
const fastBadge = queuedContainer.querySelector(".card-execution-mode-badge") as HTMLElement;
expect(headerBadges).toBeTruthy();
expect(statusBadge).toHaveTextContent("Queued to plan");
expect(metaBadges).toBeTruthy();
expect(fastBadge).toBeTruthy();
expect(metaBadges.parentElement).toBe(headerBadges);
expect(headerBadges.contains(statusBadge)).toBe(true);
expect(headerBadges.contains(fastBadge)).toBe(true);
expect(getComputedStyle(metaBadges).display).toBe("contents");
expect(getComputedStyle(fastBadge).flexShrink).toBe("0");
const priorityBadge = queuedContainer.querySelector(".card-priority-badge") as HTMLElement | null;
if (priorityBadge) expect(getComputedStyle(priorityBadge).flexShrink).toBe("0");
});
it("keeps the layout-transparent meta wrapper contract in the mobile badge context", () => {
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
expectCssRuleToContain(loadedCss, ".card-meta-badges", "display: contents;");
expectCssRuleToContain(mobileSection, ".card-header-badges", "gap: calc(var(--space-xs) / 2);");
expect(mobileSection).not.toMatch(/\.card-meta-badges\s*\{/);
});
it("places a fast-mode size badge after the task id before wrapping header badges", () => { it("places a fast-mode size badge after the task id before wrapping header badges", () => {
const { container: sizedContainer } = render( const { container: sizedContainer } = render(
<TaskCard <TaskCard

View File

@@ -3873,11 +3873,11 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-footer-row-right")).toBeNull(); expect(container.querySelector(".card-footer-row-right")).toBeNull();
}); });
it("defines responsive flex-wrap styling for grouped card meta badges", () => { it("keeps grouped card meta badges layout-transparent in the shared header wrap context", () => {
const fullCss = loadAllAppCss(); const fullCss = loadAllAppCss();
expect(fullCss).toMatch(/\.card-meta-badges\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;[^}]*gap:\s*var\(--space-xs\);[^}]*\}/); expect(fullCss).toMatch(/\.card-meta-badges\s*\{[^}]*display:\s*contents;[^}]*\}/);
expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-meta-badges\s*\{[^}]*gap:\s*calc\(var\(--space-xs\) \/ 2\);[^}]*\}/); expect(fullCss).toMatch(/@media[^{]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-header-badges\s*\{[^}]*gap:\s*calc\(var\(--space-xs\) \/ 2\);[^}]*\}/);
}); });
describe("retry button on failed tasks", () => { describe("retry button on failed tasks", () => {