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:
commit
b9fdaf62ac
14 changed files with 140 additions and 116 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -310,7 +310,6 @@ export function IssuesHeader() {
|
|||
</span>
|
||||
{/* New issue */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => useModalStore.getState().open("create-issue")}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue