Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
---
id: BACK-684
title: Move the acceptance-criteria ring into the web card header
status: To Do
assignee: []
status: In Progress
assignee:
- '@claude'
created_date: '2026-09-02 21:40'
updated_date: '2026-09-02 22:07'
labels: []
dependencies: []
ordinal: 316000
Expand All @@ -19,15 +21,36 @@ Move the ring into that header row on the right, beside the priority badge, so t

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 On board cards the acceptance-criteria ring renders in the header row on the right, beside the priority badge, and no longer occupies its own line under the title
- [ ] #2 Cards without progress or without a priority badge keep their current header layout
- [ ] #3 The task list rendering of the ring is unchanged
- [ ] #4 A rendering test covers a card with both a priority badge and progress
- [x] #1 On board cards the acceptance-criteria ring renders in the header row on the right, beside the priority badge, and no longer occupies its own line under the title
- [x] #2 Cards without progress or without a priority badge keep their current header layout
- [x] #3 The task list rendering of the ring is unchanged
- [x] #4 A rendering test covers a card with both a priority badge and progress
<!-- AC:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [ ] #1 bunx tsc --noEmit passes when TypeScript touched
- [ ] #2 bun run check . passes when formatting/linting touched
- [ ] #3 bun test (or scoped test) passes
- [x] #1 bunx tsc --noEmit passes when TypeScript touched
- [x] #2 bun run check . passes when formatting/linting touched
- [x] #3 bun test (or scoped test) passes
<!-- DOD:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. In src/web/components/TaskCard.tsx compute the priority badge once (getPriorityBadge(task.priority)) alongside the existing acceptanceCriteriaProgress counts.
2. Replace the header row's inline IIFE right side with a right group rendered only when there is a priority badge or acceptance-criteria progress: a shrink-0 flex row holding the priority badge (unchanged classes) and <AcceptanceCriteriaProgress task={task} density="card" />. Left id/type/project group keeps min-w-0.
3. Remove the standalone <AcceptanceCriteriaProgress ... className="mt-2" /> line under the title; the card gets shorter instead of taller.
4. Leave AcceptanceCriteriaProgress and the TaskList density="list" usage untouched; no helper text, nothing reserved when either side is absent.
5. Extend src/test/web-task-acceptance-progress.test.tsx with header-placement cases: priority + progress (ring inside the header row, no progress node outside it), progress without priority, and neither (header unchanged, no empty right group).
6. Gates: bunx tsc --noEmit, bun run check ., bun run test.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Moved the acceptance-criteria ring from its own line under the title into the card header row's right side in src/web/components/TaskCard.tsx. getPriorityBadge is now called once into priorityBadge; the header's right group renders only when there is progress or a priority badge, so cards with neither still render nothing beside the id. The group is 'flex shrink-0 items-center gap-2' with the ring first and the priority chip last, which keeps the chip flush right exactly where cards without progress already put it, and keeps the min-w-0 id/type/project group as the only side that gives way. AcceptanceCriteriaProgress itself and the TaskList density=list usage are untouched.

Rendered verification on a local browser board (127.0.0.1, light and dark): BACK-636 (Medium priority, 0/4) shows 'BACK-636 ... ring 0/4 Med' on one header line and BACK-684 (no priority, 0/4) shows the ring flush right, with no progress node outside the header on either card. Measured in the DOM: re-inserting the old standalone line under the title grew the BACK-636 card from 101px to 129px, so the header placement saves 28px (~22%) per card with progress. Squeezing a card that carries a type badge from 260px down to 130px kept the header a single 20px line with zero overflow - the right group holds its width and the left group truncates instead of wrapping.

Extended src/test/web-task-acceptance-progress.test.tsx with four card cases (ring plus priority badge, ring without priority, priority without progress, neither). The two placement cases fail against the pre-change component, confirming they cover the move.
<!-- SECTION:NOTES:END -->
50 changes: 50 additions & 0 deletions src/test/web-task-acceptance-progress.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ const renderList = (task: Task): HTMLElement => {
const getProgress = (container: HTMLElement): HTMLElement | null =>
container.querySelector("[data-acceptance-criteria-progress]");

// The header row is the metadata line that precedes the card title.
const getHeaderRow = (container: HTMLElement): HTMLElement => {
const header = container.querySelector("h4")?.previousElementSibling as HTMLElement | null;
expect(header?.className).toContain("justify-between");
return header as HTMLElement;
};

const RING_CIRCUMFERENCE = 2 * Math.PI * 5;

const expectRing = (progress: HTMLElement | null, checked: number, total: number) => {
Expand Down Expand Up @@ -141,6 +148,49 @@ describe("browser task acceptance criteria progress", () => {
expect(getProgress(container)).toBeNull();
});

it("renders the card ring in the header row beside the priority badge", () => {
const container = renderCard(createTask({ priority: "high", acceptanceCriteriaItems: createCriteria(4, 7) }));
const header = getHeaderRow(container);
const progress = getProgress(container);

expect(container.querySelectorAll("[data-acceptance-criteria-progress]").length).toBe(1);
expect(header.contains(progress)).toBe(true);
expectRing(progress, 4, 7);

// The ring joins the priority badge in a group that never shrinks the id side of the header,
// and the badge stays flush right so it sits where cards without progress already put it.
const rightGroup = header.lastElementChild as HTMLElement;
expect(rightGroup.className).toContain("shrink-0");
expect(rightGroup.contains(progress)).toBe(true);
expect(rightGroup.lastElementChild?.textContent).toBe("High");
});

it("renders the card ring in the header row when the task has no priority", () => {
const container = renderCard(createTask({ acceptanceCriteriaItems: createCriteria(2, 3) }));
const header = getHeaderRow(container);
const progress = getProgress(container);

expect(header.contains(progress)).toBe(true);
expect(header.textContent).toBe("task-12/3");
});

it("keeps the priority badge alone in the header row when the task has no progress", () => {
const container = renderCard(createTask({ status: "To Do", priority: "medium" }));
const header = getHeaderRow(container);

expect(getProgress(container)).toBeNull();
expect(header.textContent).toBe("task-1Med");
});

it("renders nothing beside the id when the task has neither progress nor priority", () => {
const container = renderCard(createTask({ status: "To Do" }));
const header = getHeaderRow(container);

expect(getProgress(container)).toBeNull();
expect(header.children.length).toBe(1);
expect(header.textContent).toBe("task-1");
});

it("renders a partial progress ring at list density in the wide list summary", () => {
const container = renderList(createTask({ acceptanceCriteriaItems: createCriteria(4, 7) }));
const progress = getProgress(container);
Expand Down
22 changes: 12 additions & 10 deletions src/web/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ const TaskCard: React.FC<TaskCardProps> = ({
}
};

const priorityBadge = getPriorityBadge(task.priority);

return (
<div className="relative">
{/* Branch tooltip when trying to drag cross-branch task */}
Expand Down Expand Up @@ -248,14 +250,16 @@ const TaskCard: React.FC<TaskCardProps> = ({
<TaskTypeBadge type={task.type} availableTypes={availableTypes} className="min-w-0" />
<ProjectBadge project={task.project} availableProjects={availableProjects} className="min-w-0" />
</div>
{(() => {
const badge = getPriorityBadge(task.priority);
return badge ? (
<span className={`px-1.5 py-0.5 text-[10px] font-semibold rounded ${badge.bg} ${badge.text} transition-colors duration-200`}>
{badge.label}
</span>
) : null;
})()}
{(acceptanceCriteriaProgress || priorityBadge) && (
<div className="flex shrink-0 items-center gap-2">
<AcceptanceCriteriaProgress task={task} density="card" />
{priorityBadge && (
<span className={`px-1.5 py-0.5 text-[10px] font-semibold rounded ${priorityBadge.bg} ${priorityBadge.text} transition-colors duration-200`}>
{priorityBadge.label}
</span>
)}
</div>
)}
</div>

{/* Title */}
Expand All @@ -267,8 +271,6 @@ const TaskCard: React.FC<TaskCardProps> = ({
{task.title}
</h4>

<AcceptanceCriteriaProgress task={task} density="card" className="mt-2" />

{/* Labels - limit to 3 */}
{task.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
Expand Down