Merge pull request #202 from multica-ai/forrestchang/issue-status-style

feat(ui): restyle issue status and priority with colored badges
This commit is contained in:
Jiayuan Zhang 2026-03-31 03:28:25 +08:00 committed by GitHub
commit b9fdaf62ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 140 additions and 116 deletions

View file

@ -31,6 +31,7 @@
--color-info: var(--info);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-priority: var(--priority);
--color-canvas: var(--canvas);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
@ -94,6 +95,7 @@
--success: oklch(0.55 0.16 145);
--warning: oklch(0.75 0.16 85);
--info: oklch(0.55 0.18 250);
--priority: oklch(0.65 0.18 50);
--scrollbar-thumb: oklch(0.82 0.003 286);
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067);
--scrollbar-track: transparent;
@ -137,6 +139,7 @@
--success: oklch(0.65 0.15 145);
--warning: oklch(0.70 0.16 85);
--info: oklch(0.65 0.18 250);
--priority: oklch(0.70 0.18 50);
--scrollbar-thumb: oklch(1 0 0 / 15%);
--scrollbar-thumb-hover: oklch(1 0 0 / 30%);
--scrollbar-track: transparent;

View file

@ -33,7 +33,7 @@ function ActorAvatar({
<div
className={cn(
"inline-flex shrink-0 items-center justify-center rounded-full font-medium",
isAgent ? "bg-info/10 text-info" : "bg-muted text-muted-foreground",
"bg-muted text-muted-foreground",
className
)}
style={{ width: size, height: size, fontSize: size * 0.45 }}

View file

@ -149,8 +149,10 @@ export function BatchActionToolbar() {
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
>
<PriorityIcon priority={p} />
<span>{cfg.label}</span>
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${cfg.badgeBg} ${cfg.badgeText}`}>
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
{cfg.label}
</span>
</button>
);
})}

View file

@ -62,37 +62,12 @@ export function BoardCardContent({
const showBottom = showAssignee || showDueDate;
return (
<div className="rounded-lg border bg-card p-3.5 shadow-[0_1px_2px_0_rgba(0,0,0,0.03)] transition-shadow group-hover:shadow-md">
{/* Priority */}
{showPriority &&
(editable ? (
<PickerWrapper>
<PriorityPicker
priority={issue.priority}
onUpdate={handleUpdate}
trigger={
<>
<PriorityIcon priority={issue.priority} />
<span className={`text-xs font-medium ${priorityCfg.color}`}>
{priorityCfg.label}
</span>
</>
}
/>
</PickerWrapper>
) : (
<div className="flex items-center gap-1.5">
<PriorityIcon priority={issue.priority} />
<span className={`text-xs font-medium ${priorityCfg.color}`}>
{priorityCfg.label}
</span>
</div>
))}
<div className="rounded-lg border bg-card p-3.5 shadow-[0_1px_2px_0_rgba(0,0,0,0.03)] transition-shadow group-hover:shadow-sm">
{/* Row 1: Identifier */}
<p className="text-xs text-muted-foreground">{issue.identifier}</p>
{/* Title */}
<p
className={`text-sm font-medium leading-snug line-clamp-2 ${showPriority ? "mt-2" : ""}`}
>
{/* Row 2: Title */}
<p className="mt-1 text-sm font-medium leading-snug line-clamp-2">
{issue.title}
</p>
@ -103,66 +78,87 @@ export function BoardCardContent({
</p>
)}
{/* Bottom: assignee + due date */}
{showBottom && (
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center">
{showAssignee &&
(editable ? (
<PickerWrapper>
<AssigneePicker
assigneeType={issue.assignee_type}
assigneeId={issue.assignee_id}
onUpdate={handleUpdate}
trigger={
<ActorAvatar
actorType={issue.assignee_type!}
actorId={issue.assignee_id!}
size={22}
/>
}
/>
</PickerWrapper>
) : (
<ActorAvatar
actorType={issue.assignee_type!}
actorId={issue.assignee_id!}
size={22}
/>
))}
</div>
{showDueDate &&
{/* Row 3: Assignee, priority badge, due date */}
{(showAssignee || showPriority || showDueDate) && (
<div className="mt-3 flex items-center gap-2">
{showAssignee &&
(editable ? (
<PickerWrapper>
<DueDatePicker
dueDate={issue.due_date}
<AssigneePicker
assigneeType={issue.assignee_type}
assigneeId={issue.assignee_id}
onUpdate={handleUpdate}
trigger={
<span
className={`flex items-center gap-1 text-xs ${
new Date(issue.due_date!) < new Date()
? "text-destructive"
: "text-muted-foreground"
}`}
>
<CalendarDays className="size-3" />
{formatDate(issue.due_date!)}
<ActorAvatar
actorType={issue.assignee_type!}
actorId={issue.assignee_id!}
size={22}
/>
}
/>
</PickerWrapper>
) : (
<ActorAvatar
actorType={issue.assignee_type!}
actorId={issue.assignee_id!}
size={22}
/>
))}
{showPriority &&
(editable ? (
<PickerWrapper>
<PriorityPicker
priority={issue.priority}
onUpdate={handleUpdate}
trigger={
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${priorityCfg.badgeBg} ${priorityCfg.badgeText}`}>
<PriorityIcon priority={issue.priority} className="h-3 w-3" inheritColor />
{priorityCfg.label}
</span>
}
/>
</PickerWrapper>
) : (
<span
className={`flex items-center gap-1 text-xs ${
new Date(issue.due_date!) < new Date()
? "text-destructive"
: "text-muted-foreground"
}`}
>
<CalendarDays className="size-3" />
{formatDate(issue.due_date!)}
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${priorityCfg.badgeBg} ${priorityCfg.badgeText}`}>
<PriorityIcon priority={issue.priority} className="h-3 w-3" inheritColor />
{priorityCfg.label}
</span>
))}
{showDueDate && (
<div className="ml-auto">
{editable ? (
<PickerWrapper>
<DueDatePicker
dueDate={issue.due_date}
onUpdate={handleUpdate}
trigger={
<span
className={`flex items-center gap-1 text-xs ${
new Date(issue.due_date!) < new Date()
? "text-destructive"
: "text-muted-foreground"
}`}
>
<CalendarDays className="size-3" />
{formatDate(issue.due_date!)}
</span>
}
/>
</PickerWrapper>
) : (
<span
className={`flex items-center gap-1 text-xs ${
new Date(issue.due_date!) < new Date()
? "text-destructive"
: "text-muted-foreground"
}`}
>
<CalendarDays className="size-3" />
{formatDate(issue.due_date!)}
</span>
)}
</div>
)}
</div>
)}
</div>

View file

@ -43,13 +43,15 @@ export function BoardColumn({
);
return (
<div className="flex w-[280px] shrink-0 flex-col rounded-xl bg-muted/40 p-2">
<div className={`flex w-[280px] shrink-0 flex-col rounded-xl ${cfg.columnBg} p-2`}>
<div className="mb-2 flex items-center justify-between px-1.5">
{/* Left: icon + label + count */}
{/* Left: status badge + count */}
<div className="flex items-center gap-2">
<StatusIcon status={status} className="h-3.5 w-3.5" />
<span className="text-sm font-medium">{cfg.label}</span>
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-xs text-muted-foreground">
<span className={`inline-flex items-center gap-1.5 rounded px-2 py-0.5 text-xs font-semibold ${cfg.badgeBg} ${cfg.badgeText}`}>
<StatusIcon status={status} className="h-3 w-3" inheritColor />
{cfg.label}
</span>
<span className="text-xs text-muted-foreground">
{issues.length}
</span>
</div>

View file

@ -744,8 +744,10 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
key={p}
onClick={() => handleUpdateField({ priority: p })}
>
<PriorityIcon priority={p} />
{PRIORITY_CONFIG[p].label}
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${PRIORITY_CONFIG[p].badgeBg} ${PRIORITY_CONFIG[p].badgeText}`}>
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
{PRIORITY_CONFIG[p].label}
</span>
{issue.priority === p && <span className="ml-auto text-xs text-muted-foreground"></span>}
</DropdownMenuItem>
))}
@ -1213,8 +1215,10 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
<DropdownMenuContent align="start" className="w-44">
{PRIORITY_ORDER.map((p) => (
<DropdownMenuItem key={p} onClick={() => handleUpdateField({ priority: p })}>
<PriorityIcon priority={p} />
{PRIORITY_CONFIG[p].label}
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${PRIORITY_CONFIG[p].badgeBg} ${PRIORITY_CONFIG[p].badgeText}`}>
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
{PRIORITY_CONFIG[p].label}
</span>
{p === issue.priority && <Check className="ml-auto h-3.5 w-3.5" />}
</DropdownMenuItem>
))}

View file

@ -310,7 +310,6 @@ export function IssuesHeader() {
</span>
{/* New issue */}
<Button
variant="outline"
size="sm"
onClick={() => useModalStore.getState().open("create-issue")}
>

View file

@ -96,9 +96,11 @@ export function ListView({
</div>
<Accordion.Trigger className="group/trigger flex flex-1 items-center gap-2 px-2 h-full text-left outline-none">
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground transition-transform group-aria-expanded/trigger:rotate-90" />
<StatusIcon status={status} className="h-3.5 w-3.5" />
<span className="text-sm font-medium">{cfg.label}</span>
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-xs text-muted-foreground">
<span className={`inline-flex items-center gap-1.5 rounded px-2 py-0.5 text-xs font-semibold ${cfg.badgeBg} ${cfg.badgeText}`}>
<StatusIcon status={status} className="h-3 w-3" inheritColor />
{cfg.label}
</span>
<span className="text-xs text-muted-foreground">
{statusIssues.length}
</span>
</Accordion.Trigger>

View file

@ -43,8 +43,10 @@ export function PriorityPicker({
setOpen(false);
}}
>
<PriorityIcon priority={p} />
<span>{c.label}</span>
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${c.badgeBg} ${c.badgeText}`}>
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
{c.label}
</span>
</PickerItem>
);
})}

View file

@ -4,9 +4,11 @@ import { PRIORITY_CONFIG } from "@/features/issues/config";
export function PriorityIcon({
priority,
className = "",
inheritColor = false,
}: {
priority: IssuePriority;
className?: string;
inheritColor?: boolean;
}) {
const cfg = PRIORITY_CONFIG[priority];
@ -15,7 +17,7 @@ export function PriorityIcon({
return (
<svg
viewBox="0 0 16 16"
className={`h-3.5 w-3.5 text-muted-foreground shrink-0 ${className}`}
className={`h-3.5 w-3.5 ${inheritColor ? "" : "text-muted-foreground"} shrink-0 ${className}`}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
@ -31,7 +33,7 @@ export function PriorityIcon({
return (
<svg
viewBox="0 0 16 16"
className={`h-3.5 w-3.5 ${cfg.color} shrink-0 ${className}`}
className={`h-3.5 w-3.5 ${inheritColor ? "" : cfg.color} shrink-0 ${className}`}
fill="currentColor"
style={isUrgent ? { animation: "priority-pulse 2s ease-in-out infinite" } : undefined}
>

View file

@ -160,9 +160,11 @@ const STATUS_RENDERERS: Record<IssueStatus, () => React.ReactNode> = {
export function StatusIcon({
status,
className = "h-4 w-4",
inheritColor = false,
}: {
status: IssueStatus;
className?: string;
inheritColor?: boolean;
}) {
const cfg = STATUS_CONFIG[status];
const Renderer = STATUS_RENDERERS[status];
@ -171,7 +173,7 @@ export function StatusIcon({
<svg
viewBox="0 0 14 14"
fill="none"
className={`${className} ${cfg.iconColor} shrink-0`}
className={`${className} ${inheritColor ? "" : cfg.iconColor} shrink-0`}
>
<Renderer />
</svg>

View file

@ -10,11 +10,11 @@ export const PRIORITY_ORDER: IssuePriority[] = [
export const PRIORITY_CONFIG: Record<
IssuePriority,
{ label: string; bars: number; color: string }
{ label: string; bars: number; color: string; badgeBg: string; badgeText: string }
> = {
urgent: { label: "Urgent", bars: 4, color: "text-destructive" },
high: { label: "High", bars: 3, color: "text-warning" },
medium: { label: "Medium", bars: 2, color: "text-warning" },
low: { label: "Low", bars: 1, color: "text-info" },
none: { label: "No priority", bars: 0, color: "text-muted-foreground" },
urgent: { label: "Urgent", bars: 4, color: "text-destructive", badgeBg: "bg-priority", badgeText: "text-white" },
high: { label: "High", bars: 3, color: "text-warning", badgeBg: "bg-priority/80", badgeText: "text-white" },
medium: { label: "Medium", bars: 2, color: "text-warning", badgeBg: "bg-priority/15", badgeText: "text-priority" },
low: { label: "Low", bars: 1, color: "text-info", badgeBg: "bg-priority/10", badgeText: "text-priority" },
none: { label: "No priority", bars: 0, color: "text-muted-foreground", badgeBg: "bg-muted", badgeText: "text-muted-foreground" },
};

View file

@ -22,13 +22,21 @@ export const ALL_STATUSES: IssueStatus[] = [
export const STATUS_CONFIG: Record<
IssueStatus,
{ label: string; iconColor: string; hoverBg: string }
{
label: string;
iconColor: string;
hoverBg: string;
dividerColor: string;
badgeBg: string;
badgeText: string;
columnBg: string;
}
> = {
backlog: { label: "Backlog", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
todo: { label: "Todo", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
in_progress: { label: "In Progress", iconColor: "text-warning", hoverBg: "hover:bg-warning/10" },
in_review: { label: "In Review", iconColor: "text-success", hoverBg: "hover:bg-success/10" },
done: { label: "Done", iconColor: "text-info", hoverBg: "hover:bg-info/10" },
blocked: { label: "Blocked", iconColor: "text-destructive", hoverBg: "hover:bg-destructive/10" },
cancelled: { label: "Cancelled", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent" },
backlog: { label: "Backlog", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", badgeBg: "bg-muted", badgeText: "text-muted-foreground", columnBg: "bg-muted/40" },
todo: { label: "Todo", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", badgeBg: "bg-muted", badgeText: "text-muted-foreground", columnBg: "bg-muted/40" },
in_progress: { label: "In Progress", iconColor: "text-warning", hoverBg: "hover:bg-warning/10", dividerColor: "bg-warning", badgeBg: "bg-warning", badgeText: "text-white", columnBg: "bg-warning/5" },
in_review: { label: "In Review", iconColor: "text-success", hoverBg: "hover:bg-success/10", dividerColor: "bg-success", badgeBg: "bg-success", badgeText: "text-white", columnBg: "bg-success/5" },
done: { label: "Done", iconColor: "text-info", hoverBg: "hover:bg-info/10", dividerColor: "bg-info", badgeBg: "bg-info", badgeText: "text-white", columnBg: "bg-info/5" },
blocked: { label: "Blocked", iconColor: "text-destructive", hoverBg: "hover:bg-destructive/10", dividerColor: "bg-destructive", badgeBg: "bg-destructive", badgeText: "text-white", columnBg: "bg-destructive/5" },
cancelled: { label: "Cancelled", iconColor: "text-muted-foreground", hoverBg: "hover:bg-accent", dividerColor: "bg-muted-foreground/40", badgeBg: "bg-muted", badgeText: "text-muted-foreground", columnBg: "bg-muted/40" },
};

View file

@ -248,8 +248,10 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
<DropdownMenuContent align="start" className="w-44">
{PRIORITY_ORDER.map((p) => (
<DropdownMenuItem key={p} onClick={() => updatePriority(p)}>
<PriorityIcon priority={p} />
<span>{PRIORITY_CONFIG[p].label}</span>
<span className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${PRIORITY_CONFIG[p].badgeBg} ${PRIORITY_CONFIG[p].badgeText}`}>
<PriorityIcon priority={p} className="h-3 w-3" inheritColor />
{PRIORITY_CONFIG[p].label}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>