FN-7867: make task-card priority badges icon-only to stop meta-badge wrapping

Board TaskCard priority badges no longer render visible priority text, so the .card-meta-badges row cannot wrap onto a new line when a priority label widens it.

- TaskCard.tsx: drop the visible priority-label <span>, add title/aria-label with the full priority label, and keep the label reachable via a visually-hidden span for assistive tech
- TaskCard.css: update the FNXC comment on .card-priority-badge to reflect the icon-only rationale (spacing/geometry rules unchanged, still shared with the Task Detail chip/select)
- TaskCard.test.tsx: assert icon-only rendering (no visible text node/span), title/aria-label correctness, visually-hidden label content, and that the badge is absent for tasks without a priority; loosen the mocked lucide icons to forward arbitrary props
- Add a patch changeset documenting the icon-only priority badge fix

Files changed:
 .changeset/icon-only-priority-badges.md            |  7 +++++
 packages/dashboard/app/components/TaskCard.css     |  2 +-
 packages/dashboard/app/components/TaskCard.tsx     | 12 ++++++---
 .../app/components/__tests__/TaskCard.test.tsx     | 30 +++++++++++++++++-----
 4 files changed, 39 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-7867

Fusion-Task-Lineage: 5c845c23-0a9f-47b4-9a75-6c410b507ef4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 12:26:54 -07:00
parent 139ae7e4bc
commit b10f823672
4 changed files with 39 additions and 12 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task card priority badges now show icon-only so they no longer wrap to a new line.
category: fix
dev: Updates the board TaskCard priority badge to keep labels in title, aria-label, and visually-hidden text.

View File

@@ -516,7 +516,7 @@ FN-6774 removes the saturated stuck-task edge stripe from board cards. Keep the
} }
.card-priority-badge { .card-priority-badge {
/* FNXC:PriorityColorCoding 2026-07-11-00:00: Priority badges add a glyph beside the label; keep inline-flex geometry and token spacing so badge height/wrap invariants do not change. */ /* 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. */
gap: var(--space-xs); gap: var(--space-xs);
letter-spacing: 0.4px; letter-spacing: 0.4px;
} }

View File

@@ -54,7 +54,7 @@ import { WorkspaceWorktreesSummary, isWorkspaceTask } from "./WorkspaceWorktrees
import { WorkflowIcon } from "./WorkflowIcon"; import { WorkflowIcon } from "./WorkflowIcon";
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel, type TaskContextMenuColumnFlags, type TaskContextMenuColumnMetadata, type TaskMenuActionDescriptor } from "./TaskContextMenu"; import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel, type TaskContextMenuColumnFlags, type TaskContextMenuColumnMetadata, type TaskMenuActionDescriptor } from "./TaskContextMenu";
import { formatCost, hasTaskCost, taskTotalCost } from "../utils/taskTokenCost"; import { formatCost, hasTaskCost, taskTotalCost } from "../utils/taskTokenCost";
import { getPriorityColorVar, getPriorityIcon } from "../utils/priorityIndicator"; import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
/** Per-branch progress snapshot (U13). Surfaced as an optional additive field /** Per-branch progress snapshot (U13). Surfaced as an optional additive field
* on the task payload for the parallel-window badge (U9). */ * on the task payload for the parallel-window badge (U9). */
@@ -3086,10 +3086,14 @@ function TaskCardComponent({
{hasCardMetaBadges && ( {hasCardMetaBadges && (
<div className="card-meta-badges" data-testid="card-meta-badges"> <div className="card-meta-badges" data-testid="card-meta-badges">
{showPriorityBadge && ( {showPriorityBadge && (
<span className={`card-priority-badge card-priority-badge--${normalizedPriority}`}> <span
{/* FNXC:PriorityColorCoding 2026-07-11-00:00: Cards render the shared priority glyph with the priorityIndicator urgency color while preserving the existing badge text and non-normal visibility gate. */} className={`card-priority-badge card-priority-badge--${normalizedPriority}`}
title={getPriorityLabel(normalizedPriority)}
aria-label={getPriorityLabel(normalizedPriority)}
>
{/* FNXC:PriorityIconOnlyBadge 2026-07-12-00:00: FN-7867 makes task-card priority badges icon-only so priority text cannot widen .card-meta-badges and force wrapping; preserve the label through title, aria-label, and visually-hidden text while keeping the shared urgency color. */}
<PriorityBadgeIcon size={10} aria-hidden="true" style={{ color: getPriorityColorVar(normalizedPriority) }} /> <PriorityBadgeIcon size={10} aria-hidden="true" style={{ color: getPriorityColorVar(normalizedPriority) }} />
<span>{normalizedPriority}</span> <span className="visually-hidden">{getPriorityLabel(normalizedPriority)}</span>
</span> </span>
)} )}
{task.executionMode === "fast" && ( {task.executionMode === "fast" && (

View File

@@ -21,7 +21,7 @@ import { NavigationHistoryProvider, useNavigationHistory } from "../../hooks/use
import { useOverlayDismiss } from "../../hooks/useOverlayDismiss"; import { useOverlayDismiss } from "../../hooks/useOverlayDismiss";
import type { ConfirmOptions } from "../../hooks/useConfirm"; import type { ConfirmOptions } from "../../hooks/useConfirm";
import { TASK_PRIORITIES, type Task, type TaskPriority } from "@fusion/core"; import { TASK_PRIORITIES, type Task, type TaskPriority } from "@fusion/core";
import { getPriorityColorVar } from "../../utils/priorityIndicator"; import { getPriorityColorVar, getPriorityLabel } from "../../utils/priorityIndicator";
// Mock lucide-react to avoid SVG rendering issues in test env // Mock lucide-react to avoid SVG rendering issues in test env
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
@@ -42,10 +42,10 @@ vi.mock("lucide-react", () => ({
RotateCw: () => null, RotateCw: () => null,
Zap: () => <svg data-testid="icon-zap" />, Zap: () => <svg data-testid="icon-zap" />,
AlertTriangle: () => null, AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-low" className="lucide-arrow-down" style={style} />, ArrowDown: ({ style, ...props }: React.SVGProps<SVGSVGElement>) => <svg data-testid="priority-icon-low" className="lucide-arrow-down" style={style} {...props} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-normal" className="lucide-flag" style={style} />, Flag: ({ style, ...props }: React.SVGProps<SVGSVGElement>) => <svg data-testid="priority-icon-normal" className="lucide-flag" style={style} {...props} />,
ArrowUp: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-high" className="lucide-arrow-up" style={style} />, ArrowUp: ({ style, ...props }: React.SVGProps<SVGSVGElement>) => <svg data-testid="priority-icon-high" className="lucide-arrow-up" style={style} {...props} />,
TriangleAlert: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-urgent" className="lucide-triangle-alert" style={style} />, TriangleAlert: ({ style, ...props }: React.SVGProps<SVGSVGElement>) => <svg data-testid="priority-icon-urgent" className="lucide-triangle-alert" style={style} {...props} />,
ArrowUpRight: () => null, ArrowUpRight: () => null,
// FN-7592: the overseer badge now renders an icon child instead of a text label, // FN-7592: the overseer badge now renders an icon child instead of a text label,
// so tests must see a real SVG (like Zap) rather than a no-op render. // so tests must see a real SVG (like Zap) rather than a no-op render.
@@ -2790,7 +2790,7 @@ describe("TaskCard", () => {
]); ]);
}); });
it("renders shared urgency-colored glyphs for visible priority badges while normal stays hidden", () => { it("renders icon-only urgency-colored priority badges with accessible labels while normal stays hidden", () => {
for (const priority of TASK_PRIORITIES) { for (const priority of TASK_PRIORITIES) {
const { container, unmount } = render( const { container, unmount } = render(
<TaskCard <TaskCard
@@ -2807,13 +2807,29 @@ describe("TaskCard", () => {
continue; continue;
} }
const label = getPriorityLabel(priority);
expect(badge).not.toBeNull(); expect(badge).not.toBeNull();
expect(badge).toHaveTextContent(priority); expect(badge).toHaveAttribute("aria-label", label);
expect(badge).toHaveAttribute("title", label);
expect(Array.from(badge?.childNodes ?? []).filter((node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim())).toEqual([]);
const visibleLabelSpans = Array.from(badge?.querySelectorAll("span") ?? []).filter((span) => !span.classList.contains("visually-hidden"));
expect(visibleLabelSpans).toEqual([]);
expect(badge?.querySelector(".visually-hidden")).toHaveTextContent(label);
const icon = badge?.querySelector("svg"); const icon = badge?.querySelector("svg");
expect(icon).not.toBeNull(); expect(icon).not.toBeNull();
expect(icon).toHaveAttribute("aria-hidden", "true");
expect(icon?.getAttribute("style")).toContain(`color: ${getPriorityColorVar(priority)}`); expect(icon?.getAttribute("style")).toContain(`color: ${getPriorityColorVar(priority)}`);
unmount(); unmount();
} }
const { container } = render(
<TaskCard
task={makeTask({ priority: undefined })}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-priority-badge")).toBeNull();
}); });
it("renders partial card meta groups without empty wrappers when time is absent", () => { it("renders partial card meta groups without empty wrappers when time is absent", () => {