From 46d5019e2d66b28005ec9f9ea095a0e3a20a10d3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 14:26:58 -0700 Subject: [PATCH] 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) --- .changeset/fn-8631-task-card-bottom-space.md | 7 + .../dashboard/app/components/TaskCard.css | 8 +- .../components/__tests__/TaskCard.test.tsx | 140 ++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 .changeset/fn-8631-task-card-bottom-space.md diff --git a/.changeset/fn-8631-task-card-bottom-space.md b/.changeset/fn-8631-task-card-bottom-space.md new file mode 100644 index 0000000000..0105e27841 --- /dev/null +++ b/.changeset/fn-8631-task-card-bottom-space.md @@ -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. diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 09b43c5736..4a581fd07e 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -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; diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index acce10aff7..0fcbd36fcd 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -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(), + assert: (container: HTMLElement) => expect(container.querySelector(".card-steps-toggle")).not.toBeNull(), + }, + { + name: "no progress, meta, or action row", + renderCard: () => render(), + 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( + + )} + onOpenDetail={noop} + addToast={noop} + onPromote={vi.fn()} + /> + , + ), + assert: (container: HTMLElement) => expect(container.querySelector(".card-promote-cost-row .card-cost-indicator")).not.toBeNull(), + }, + { + name: "workflow and agent rows", + renderCard: () => render( + , + ), + 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(); + 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( + , + ); + // 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" };