FN-7592: replace overseer badge text label with a colored Eye icon
Swaps the planner-overseer status badge from an uppercase text pill to a compact icon glyph, keeping accessibility text on aria-label/title. - Render a small lucide-react Eye icon instead of the state-label text inside the overseer badge - Keep the readable state name on aria-label and the composed tooltip on title for accessibility - Add per-state coloring (watching/steering/recovering/awaiting-confirmation) keyed off the data-planner-overseer-state attribute in TaskCard.css, sized tightly around the icon - Update TaskCard tests to assert the icon renders and the accessible name moved to aria-label instead of textContent Files changed: packages/dashboard/app/components/TaskCard.css | 40 ++++++++++++++++++++++ packages/dashboard/app/components/TaskCard.tsx | 14 ++++++-- .../app/components/__tests__/TaskCard.test.tsx | 25 +++++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7592 Fusion-Task-Lineage: d7ca93a6-9236-4fa0-a829-80f5b99dbd5f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -397,6 +397,46 @@ not a restyle of the ordinary manual-approval badge.
|
||||
border-color: color-mix(in srgb, var(--color-warning) 45%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
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).
|
||||
*/
|
||||
.card-planner-overseer-state {
|
||||
padding: calc(var(--space-xs) / 2);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.card-planner-overseer-state svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.card-planner-overseer-state[data-planner-overseer-state="watching"] {
|
||||
background: color-mix(in srgb, var(--color-info) 15%, transparent);
|
||||
color: var(--color-info);
|
||||
border-color: color-mix(in srgb, var(--color-info) 40%, transparent);
|
||||
}
|
||||
|
||||
.card-planner-overseer-state[data-planner-overseer-state="steering"],
|
||||
.card-planner-overseer-state[data-planner-overseer-state="recovering"] {
|
||||
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
||||
color: var(--color-warning);
|
||||
border-color: color-mix(in srgb, var(--color-warning) 45%, transparent);
|
||||
}
|
||||
|
||||
.card-planner-overseer-state[data-planner-overseer-state="awaiting-confirmation"] {
|
||||
background: color-mix(in srgb, var(--triage) 18%, transparent);
|
||||
color: var(--triage);
|
||||
border-color: color-mix(in srgb, var(--triage) 45%, transparent);
|
||||
}
|
||||
|
||||
.card.awaiting-input {
|
||||
border-left: 3px solid var(--color-warning);
|
||||
background: color-mix(in srgb, var(--color-warning) 6%, transparent);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { memo, useCallback, useState, useRef, useEffect, useLayoutEffect, useMemo, type CSSProperties, type ReactElement } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight } from "lucide-react";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight, Eye } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column, ColumnId, PrInfo, IssueInfo, TaskPriority, GithubIssueAction, MergeResult, PlannerOversightLevel } from "@fusion/core";
|
||||
import {
|
||||
DEFAULT_PLANNER_OVERSIGHT_LEVEL,
|
||||
@@ -2889,15 +2889,25 @@ function TaskCardComponent({
|
||||
board payload) plus a repaint-correct memo comparator; FN-7516 owns the styled
|
||||
badge/design and surface-by-surface rendering. This is a minimal, type-safe,
|
||||
guarded read only — nothing renders for an absent field or the "idle" state.
|
||||
|
||||
FNXC:PlannerOversight 2026-07-05-00:00:
|
||||
FN-7592 replaces the uppercase text label with a small state-colored `Eye` icon so
|
||||
the badge reads as a compact glyph. The readable label and composed tooltip stay
|
||||
available for accessibility: `aria-label` carries the state name (screen readers)
|
||||
and `title` keeps the existing tooltip (hover). Per-state color comes from the
|
||||
`data-planner-overseer-state` attribute in TaskCard.css — do not fork the label
|
||||
logic here; `plannerOverseerStateLabel`/`plannerOverseerBadgeTooltip` remain the
|
||||
single source of truth.
|
||||
*/}
|
||||
{task.plannerOverseerState && task.plannerOverseerState.state !== "idle" && (
|
||||
<span
|
||||
className="card-status-badge card-planner-overseer-state"
|
||||
title={plannerOverseerBadgeTooltip(task.plannerOverseerState, t)}
|
||||
aria-label={plannerOverseerStateLabel(task.plannerOverseerState.state, t)}
|
||||
data-testid="planner-overseer-state-badge"
|
||||
data-planner-overseer-state={task.plannerOverseerState.state}
|
||||
>
|
||||
{plannerOverseerStateLabel(task.plannerOverseerState.state, t)}
|
||||
<Eye aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
{showStalledReview && stalledReview && (
|
||||
|
||||
@@ -26,7 +26,9 @@ vi.mock("lucide-react", () => ({
|
||||
Zap: () => <svg data-testid="icon-zap" />,
|
||||
AlertTriangle: () => null,
|
||||
ArrowUpRight: () => null,
|
||||
Eye: () => null,
|
||||
// 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.
|
||||
Eye: () => <svg data-testid="icon-eye" />,
|
||||
}));
|
||||
|
||||
vi.mock("../ProviderIcon", () => ({
|
||||
@@ -317,9 +319,13 @@ describe("TaskCard", () => {
|
||||
|
||||
render(<TaskCard task={task} onOpenDetail={noop} addToast={noop} />);
|
||||
|
||||
// FN-7592: the badge is now an icon-only glyph. The readable label moved from
|
||||
// textContent to aria-label; the composed tooltip is unchanged on title.
|
||||
const badge = screen.getByTestId("planner-overseer-state-badge");
|
||||
expect(badge.textContent).not.toBe("awaiting-confirmation");
|
||||
expect(badge.textContent).toBe("Awaiting confirmation");
|
||||
expect(badge.querySelector("svg")).toBeInTheDocument();
|
||||
expect(badge.getAttribute("aria-label")).not.toBe("awaiting-confirmation");
|
||||
expect(badge.getAttribute("aria-label")).toBe("Awaiting confirmation");
|
||||
expect(badge.getAttribute("data-planner-overseer-state")).toBe("awaiting-confirmation");
|
||||
|
||||
const title = badge.getAttribute("title") ?? "";
|
||||
expect(title).not.toBe("Planner overseer: awaiting-confirmation");
|
||||
@@ -343,8 +349,12 @@ describe("TaskCard", () => {
|
||||
},
|
||||
});
|
||||
const { unmount } = render(<TaskCard task={watchingTask} onOpenDetail={noop} addToast={noop} />);
|
||||
// FN-7592: icon-only badge — assert the accessible name via aria-label and the
|
||||
// per-state color hook via data-planner-overseer-state, not raw text content.
|
||||
let badge = screen.getByTestId("planner-overseer-state-badge");
|
||||
expect(badge.textContent).toBe("Overseer watching");
|
||||
expect(badge.querySelector("svg")).toBeInTheDocument();
|
||||
expect(badge.getAttribute("aria-label")).toBe("Overseer watching");
|
||||
expect(badge.getAttribute("data-planner-overseer-state")).toBe("watching");
|
||||
expect(badge.getAttribute("title")).not.toMatch(/undefined/);
|
||||
unmount();
|
||||
|
||||
@@ -361,7 +371,12 @@ describe("TaskCard", () => {
|
||||
});
|
||||
render(<TaskCard task={recoveringTask} onOpenDetail={noop} addToast={noop} />);
|
||||
badge = screen.getByTestId("planner-overseer-state-badge");
|
||||
expect(badge.textContent).toBe("Overseer recovering");
|
||||
expect(badge.querySelector("svg")).toBeInTheDocument();
|
||||
expect(badge.getAttribute("aria-label")).toBe("Overseer recovering");
|
||||
// Distinct states expose distinct data-planner-overseer-state values, which is the
|
||||
// hook TaskCard.css keys per-state color off of (jsdom cannot compute color-mix()).
|
||||
expect(badge.getAttribute("data-planner-overseer-state")).toBe("recovering");
|
||||
expect(badge.getAttribute("data-planner-overseer-state")).not.toBe("watching");
|
||||
const title = badge.getAttribute("title") ?? "";
|
||||
expect(title).not.toMatch(/undefined/);
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
|
||||
Reference in New Issue
Block a user