Align task property icons with assignee avatars (#13319)

## Thinking Path

> - Paperclip is the open source app that people use to manage AI agents
for work.
> - The task properties panel helps operators inspect and update task
data.
> - The status, assignee, and project rows used different leading icon
sizes.
> - The mixed sizes made the rows look uneven.
> - The assignee avatar already provides the correct 24px visual size.
> - This pull request sets the status and project visuals to that same
size.
> - The benefit is a tidy and consistent properties panel.

## Linked Issues or Issue Description

**What happened?**

The task properties panel showed a 12px status icon, a 24px assignee
avatar, and a 16px project tile.

**Expected behavior**

The status, assignee, and project rows must use the same 24px leading
visual size.

**Steps to reproduce**

1. Open a task.
2. Open the Properties panel.
3. Compare the Status, Assignee, and Project rows.

## What Changed

- Keep the Status glyph at 16px and use spacing to give it the 24px
avatar footprint.
- Set the Project row tile to the 24px small tile size.
- Add a regression check for the Status row size.

## Verification

- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx` (72
tests passed)
- `pnpm check:token-gates` (all gates clean)
- `pnpm --filter @paperclipai/ui typecheck` (passed)
- Full repository typecheck and build reached the Rust runner step, but
this environment does not contain `cargo`.
- The full test command was stopped after unrelated chat integration
tests did not finish. The focused UI suite passed.

## Risks

- Low risk. This change only changes visual sizes in three property
rows.
- The wider status footprint can use slightly more horizontal space in a
narrow panel.

> This fix does not overlap with planned core work in `ROADMAP.md`.

## Model Used

- OpenAI Codex, GPT-5.6, tool use and code execution enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-09-27 11:35:39 +00:00
committed by GitHub
co-authored by Paperclip
parent d423fc4411
commit 7e0f051d3b
4 changed files with 31 additions and 7 deletions
+8 -2
View File
@@ -130,8 +130,8 @@ vi.mock("../lib/assignees", () => ({
}));
vi.mock("./StatusIcon", () => ({
StatusIcon: ({ status, blockerAttention }: { status: string; blockerAttention?: Issue["blockerAttention"] }) => (
<span data-status-icon-state={blockerAttention?.state}>{status}</span>
StatusIcon: ({ status, blockerAttention, className, glyphContainerClassName, size }: { status: string; blockerAttention?: Issue["blockerAttention"]; className?: string; glyphContainerClassName?: string; size?: string }) => (
<span className={className} data-glyph-container-class={glyphContainerClassName} data-testid="status-icon" data-size={size} data-status-icon-state={blockerAttention?.state}>{status}</span>
),
}));
@@ -527,6 +527,12 @@ describe("IssueProperties", () => {
expect(surface?.classList).toContain("pl-4");
expect(surface?.querySelectorAll('[data-property-section="true"]').length).toBeGreaterThan(1);
expect(surface?.querySelector('[data-property-value="true"]')).not.toBeNull();
const statusVisual = surface?.querySelector(
'[data-property-label="Status"] + [data-property-value="true"] [data-testid="status-icon"]',
);
expect(statusVisual).not.toBeNull();
expect(statusVisual?.getAttribute("data-size")).toBeNull();
expect(statusVisual?.getAttribute("data-glyph-container-class")).toContain("size-6");
expect(surface?.querySelector('[data-property-section="true"] > div')?.classList)
.toContain("text-muted-foreground/70");
const projectLabel = surface?.querySelector('[data-property-label="Project"]');
+10
View File
@@ -108,6 +108,16 @@ describe("StatusIcon — glyph size (PAP-243a)", () => {
expect(html).toContain('width="16"');
expect(html).toContain('height="16"');
});
it("can place the default glyph in a larger layout footprint without resizing it", () => {
const html = renderToStaticMarkup(
<StatusIcon status="todo" glyphContainerClassName="size-6" showLabel />,
);
expect(html).toContain('data-status-glyph-container="true"');
expect(html).toContain('class="size-6"');
expect(html).toContain('width="16"');
expect(html).toContain('height="16"');
});
});
+9 -2
View File
@@ -17,6 +17,8 @@ interface StatusIconProps {
blockerAttention?: IssueBlockerAttention | null;
onChange?: (status: string) => void;
className?: string;
/** Optional layout wrapper around the glyph. Does not change glyph dimensions. */
glyphContainerClassName?: string;
showLabel?: boolean;
/** Glyph size (PAP-243a). Default `md` (16px); lists/detail/mentions use `lg` (20px). */
size?: StatusGlyphSize;
@@ -76,14 +78,14 @@ function blockedAttentionLabel(blockerAttention: IssueBlockerAttention | null |
* glyph — the blocked shape recoloured blue — while the full blocked reason
* still rides on the accessible label.
*/
export function StatusIcon({ status, externalConversationState, blockerAttention, onChange, className, showLabel, size = "md" }: StatusIconProps) {
export function StatusIcon({ status, externalConversationState, blockerAttention, onChange, className, glyphContainerClassName, showLabel, size = "md" }: StatusIconProps) {
const [open, setOpen] = useState(false);
const displayStatus = status === "in_review" && externalConversationState === "waiting" ? "idle" : status;
const isCoveredBlocked = status === "blocked" && blockerAttention?.state === "covered";
const ariaLabel = status === "blocked" ? blockedAttentionLabel(blockerAttention) : statusLabel(displayStatus);
const glyphStatus = isCoveredBlocked ? "in_queue" : displayStatus;
const glyph = (
const glyphIcon = (
<StatusGlyph
status={glyphStatus}
size={size}
@@ -91,6 +93,11 @@ export function StatusIcon({ status, externalConversationState, blockerAttention
title={ariaLabel}
/>
);
const glyph = glyphContainerClassName ? (
<span className={glyphContainerClassName} data-status-glyph-container="true">
{glyphIcon}
</span>
) : glyphIcon;
if (!onChange) {
return showLabel ? (
@@ -1947,7 +1947,7 @@ export function IssueProperties({
<ProjectTile
color={issueProject?.color ?? null}
icon={issueProject?.icon ?? null}
size="xs"
size="sm"
/>
<span className="text-sm truncate min-w-0" title={projectName(issue.projectId)}>{projectName(issue.projectId)}</span>
</>
@@ -2334,8 +2334,9 @@ export function IssueProperties({
>
<PropertyRow label="Status">
<StatusIcon
status={issue.status} externalConversationState={issue.externalConversationState}
className="size-3"
status={issue.status}
externalConversationState={issue.externalConversationState}
glyphContainerClassName="inline-flex size-6 shrink-0 items-center justify-center"
blockerAttention={issue.blockerAttention}
onChange={(status) => onUpdate({ status })}
showLabel