feat(KB-017): add size badge to TaskCard component

- Add SizeBadge sub-component to display task size (XS/S/M/L/XL) in TaskCard

- Add CSS styling with color-coded badges for each size level

- Add comprehensive tests for size badge rendering across all size variants
This commit is contained in:
gsxdsm
2026-03-29 18:33:53 -07:00
parent af2d577e5f
commit 70a28b54a7
3 changed files with 133 additions and 0 deletions

View File

@@ -167,6 +167,12 @@ export function TaskCard({ task, queued, onOpenDetail, addToast, globalPaused, t
#{task.prInfo.number}
</span>
)}
{/* Size Indicator */}
{task.size && (
<span className={`card-size-badge size-${task.size.toLowerCase()}`}>
{task.size}
</span>
)}
</div>
<div className="card-title">
{task.title || (task.description ? task.description.slice(0, 60) + (task.description.length > 60 ? "…" : "") : task.id)}

View File

@@ -439,3 +439,99 @@ describe("TaskCard clickable dependencies", () => {
expect(depBadge.classList.contains("clickable")).toBe(true);
});
});
/**
* Component tests for size badge rendering in TaskCard.
*/
describe("TaskCard size badge", () => {
const noopToast = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("renders size badge when task.size is 'S'", () => {
const task = makeTask({ size: "S" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const sizeBadge = screen.getByText("S");
expect(sizeBadge).toBeDefined();
expect(sizeBadge.classList.contains("card-size-badge")).toBe(true);
expect(sizeBadge.classList.contains("size-s")).toBe(true);
});
it("renders size badge when task.size is 'M'", () => {
const task = makeTask({ size: "M" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const sizeBadge = screen.getByText("M");
expect(sizeBadge).toBeDefined();
expect(sizeBadge.classList.contains("card-size-badge")).toBe(true);
expect(sizeBadge.classList.contains("size-m")).toBe(true);
});
it("renders size badge when task.size is 'L'", () => {
const task = makeTask({ size: "L" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const sizeBadge = screen.getByText("L");
expect(sizeBadge).toBeDefined();
expect(sizeBadge.classList.contains("card-size-badge")).toBe(true);
expect(sizeBadge.classList.contains("size-l")).toBe(true);
});
it("does NOT render size badge when task.size is undefined", () => {
const task = makeTask({ size: undefined });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const sizeBadge = screen.queryByText(/^[SML]$/);
expect(sizeBadge).toBeNull();
});
it("positioned in card-header with other badges", () => {
const task = makeTask({ size: "M", status: "executing" });
const { container } = render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const cardHeader = container.querySelector(".card-header");
expect(cardHeader).toBeDefined();
const sizeBadge = cardHeader?.querySelector(".card-size-badge");
expect(sizeBadge).toBeDefined();
expect(sizeBadge?.textContent).toBe("M");
});
});

View File

@@ -443,6 +443,37 @@ body {
color: #da3633;
}
/* Size badge: positioned in card header, subtle color coding for effort estimation */
.card-size-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 2px 8px;
border-radius: 10px;
margin-left: auto;
}
/* Size S: subtle green tint (low effort) */
.card-size-badge.size-s {
background: rgba(63, 185, 80, 0.15);
color: var(--color-success);
}
/* Size M: neutral tint (medium effort) */
.card-size-badge.size-m {
background: rgba(139, 148, 158, 0.15);
color: var(--text-muted);
}
/* Size L: subtle amber tint (higher effort) */
.card-size-badge.size-l {
background: rgba(210, 153, 34, 0.15);
color: var(--triage);
}
.card.failed {
border-left: 3px solid #da3633;
}