FN-8631: remove task card bottom whitespace
Make progress-bearing task cards use their content height without an unused trailing band. - Remove the fixed minimum height from the task-card steps toggle. - Cover trailing-row layout across desktop and mobile task-card variants. - Add a patch changeset for the visual layout fix. Files changed: .changeset/fn-8631-task-card-bottom-space.md | 7 ++ packages/dashboard/app/components/TaskCard.css | 8 +- .../app/components/__tests__/TaskCard.test.tsx | 140 +++++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8631 Fusion-Task-Lineage: 408d359f-66ed-4510-8974-3debbf76860f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8631-task-card-bottom-space.md
Normal file
7
.changeset/fn-8631-task-card-bottom-space.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Remove excess blank space below task-card progress controls.
|
||||
category: fix
|
||||
dev: The steps toggle now uses its content height instead of a fixed trailing minimum.
|
||||
@@ -1350,13 +1350,17 @@ so the icon needs no rule of its own.
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Steps toggle and list */
|
||||
/*
|
||||
FNXC:TaskCardLayout 2026-07-31-20:57:
|
||||
FN-8631 requires the steps toggle to be content-sized: a fixed minimum height made its unused
|
||||
vertical space look like a dead band at the bottom of cards with progress. Token padding preserves
|
||||
the established compact mobile interaction rhythm while the card remains the primary touch target.
|
||||
*/
|
||||
.card-steps-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin-top: 6px;
|
||||
min-height: calc(var(--space-xl) + var(--space-md));
|
||||
padding: var(--space-xs) 0;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -8354,6 +8354,146 @@ VERIFIED UNCOVERED rather than assumed: mutating `canEdit` back to the hardcoded
|
||||
nothing caught it. These four assert the real `aria-label`, and that mutation now fails with
|
||||
"Unable to find an accessible element ... name 'Edit task'".
|
||||
*/
|
||||
/*
|
||||
FNXC:TaskCardLayout 2026-07-31-20:57:
|
||||
FN-8631 protects the board-density contract at both supported card breakpoints. jsdom has no layout
|
||||
engine, so this suite enforces the structural form of the visual invariant: progress toggles are
|
||||
content-sized and no known trailing row mounts without visible content.
|
||||
*/
|
||||
describe("TaskCard trailing-row layout (FN-8631)", () => {
|
||||
const trailingRowSelectors = [
|
||||
".card-meta",
|
||||
".card-agent-row",
|
||||
".card-action-row",
|
||||
".card-promote-cost-row",
|
||||
".card-agent-badge-row",
|
||||
".card-workflow-badge-row",
|
||||
];
|
||||
const originalInnerWidth = window.innerWidth;
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
|
||||
function setCardBreakpoint(width: number) {
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
|
||||
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
||||
matches: query.includes("max-width: 768px") ? width <= 768 : false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})) as unknown as typeof window.matchMedia;
|
||||
}
|
||||
|
||||
function expectContentBackedTrailingRows(container: HTMLElement) {
|
||||
for (const selector of trailingRowSelectors) {
|
||||
for (const row of Array.from(container.querySelectorAll(selector))) {
|
||||
expect(row.children.length, `${selector} must not render as an empty trailing shell`).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: originalInnerWidth });
|
||||
window.matchMedia = originalMatchMedia;
|
||||
});
|
||||
|
||||
it.each([1280, 390])("keeps every trailing-card variant content-backed at %ipx", (width) => {
|
||||
setCardBreakpoint(width);
|
||||
const cleanupCss = mountCssForBadgeTests();
|
||||
try {
|
||||
const progressTask = makeTask({
|
||||
id: `FN-progress-${width}`,
|
||||
column: "todo",
|
||||
status: "executing" as any,
|
||||
steps: [{ name: "Implementation", status: "in-progress" }],
|
||||
});
|
||||
const variants = [
|
||||
{
|
||||
name: "collapsed progress",
|
||||
renderCard: () => render(<TaskCard task={progressTask} onOpenDetail={noop} addToast={noop} />),
|
||||
assert: (container: HTMLElement) => expect(container.querySelector(".card-steps-toggle")).not.toBeNull(),
|
||||
},
|
||||
{
|
||||
name: "no progress, meta, or action row",
|
||||
renderCard: () => render(<TaskCard task={makeTask({ id: `FN-minimal-${width}`, column: "todo" })} onOpenDetail={noop} addToast={noop} />),
|
||||
assert: (container: HTMLElement) => {
|
||||
expect(container.querySelector(".card-steps-toggle")).toBeNull();
|
||||
expect(container.querySelector(".card-meta")).toBeNull();
|
||||
expect(container.querySelector(".card-action-row")).toBeNull();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "promote cost",
|
||||
renderCard: () => render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
id: `FN-cost-${width}`,
|
||||
column: "todo",
|
||||
tokenUsage: { inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000, firstUsedAt: "2026-01-01T00:00:00Z", lastUsedAt: "2026-01-01T00:00:00Z", modelProvider: "openai", modelId: "gpt-5-mini" },
|
||||
} as Partial<Task>)}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onPromote={vi.fn()}
|
||||
/>
|
||||
</CostBadgeProvider>,
|
||||
),
|
||||
assert: (container: HTMLElement) => expect(container.querySelector(".card-promote-cost-row .card-cost-indicator")).not.toBeNull(),
|
||||
},
|
||||
{
|
||||
name: "workflow and agent rows",
|
||||
renderCard: () => render(
|
||||
<TaskCard
|
||||
task={makeTask({ id: `FN-workflow-agent-${width}`, modelProvider: "openai" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
workflowBadge={{ workflowId: "wf-1", workflowName: "Workflow" }}
|
||||
/>,
|
||||
),
|
||||
assert: (container: HTMLElement) => {
|
||||
expect(container.querySelector(".card-agent-row")).not.toBeNull();
|
||||
expect(container.querySelector(".card-workflow-badge-row")).not.toBeNull();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const variant of variants) {
|
||||
const view = variant.renderCard();
|
||||
variant.assert(view.container);
|
||||
expectContentBackedTrailingRows(view.container);
|
||||
view.unmount();
|
||||
}
|
||||
|
||||
const expanded = render(<TaskCard task={progressTask} onOpenDetail={noop} addToast={noop} />);
|
||||
fireEvent.click(expanded.container.querySelector(".card-steps-toggle") as HTMLButtonElement);
|
||||
expect(expanded.container.querySelector(".card-steps-list")).not.toBeNull();
|
||||
expectContentBackedTrailingRows(expanded.container);
|
||||
expanded.unmount();
|
||||
|
||||
const editing = render(
|
||||
<TaskCard task={makeTask({ id: `FN-editing-${width}`, column: "todo" })} onOpenDetail={noop} addToast={noop} onUpdateTask={noop} />,
|
||||
);
|
||||
// Editing returns early with only edit content, so none of the normal trailing rows can leave an empty shell.
|
||||
fireEvent.click(editing.container.querySelector(".card-edit-btn") as HTMLButtonElement);
|
||||
expect(editing.container.querySelector(".card-editing")).not.toBeNull();
|
||||
for (const selector of trailingRowSelectors) expect(editing.container.querySelector(selector)).toBeNull();
|
||||
|
||||
const css = loadAllAppCss();
|
||||
const stepsToggleRule = css.match(/\.card-steps-toggle\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
expect(stepsToggleRule).not.toContain("min-height");
|
||||
expect(declaredStyle(".card-steps-toggle", "padding")).toBe("var(--space-xs) 0");
|
||||
if (width <= 768) {
|
||||
// FN-4351: mobile keeps the existing compact, token-sized toggle rather than adding a fixed minimum.
|
||||
expect(stepsToggleRule).toContain("padding: var(--space-xs) 0");
|
||||
}
|
||||
} finally {
|
||||
cleanupCss();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard field editability resolves column traits (U12 — R8)", () => {
|
||||
const EDIT_LABEL = { name: "Edit task" };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user