From 8fd149231adb5b8e02048c91fb4b3f20538d7767 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sat, 21 Mar 2026 14:24:02 +0800 Subject: [PATCH] refactor(web): polish Issues page UI to match Linear design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status: use Lucide circle-variant icons (CircleDashed, Circle, CircleDot, Eye, CircleCheck) instead of colored dots - Priority: use SVG bar indicators (4-bar urgent → 1-bar low) instead of P0/P1 text badges - Board cards: cleaner layout, tighter spacing, comment count icon - List rows: compact 36px height, proper alignment - Toolbar: smaller, denser header with refined toggle Co-Authored-By: Claude Opus 4.6 --- apps/web/app/(dashboard)/issues/page.tsx | 213 +++++++++++++---------- 1 file changed, 123 insertions(+), 90 deletions(-) diff --git a/apps/web/app/(dashboard)/issues/page.tsx b/apps/web/app/(dashboard)/issues/page.tsx index f4d20729..7bda7c70 100644 --- a/apps/web/app/(dashboard)/issues/page.tsx +++ b/apps/web/app/(dashboard)/issues/page.tsx @@ -7,12 +7,19 @@ import { List, Plus, Bot, - Calendar, + Circle, + CircleDashed, + CircleDot, + CircleCheck, + CircleX, + CircleAlert, + Eye, + Minus, + MessageSquare, } from "lucide-react"; import type { IssueStatus, IssuePriority } from "@multica/types"; import { MOCK_ISSUES, - STATUS_ORDER, STATUS_CONFIG, PRIORITY_CONFIG, type MockIssue, @@ -20,31 +27,83 @@ import { } from "./_data/mock"; // --------------------------------------------------------------------------- -// Shared sub-components +// Shared icon components // --------------------------------------------------------------------------- -function PriorityBadge({ priority }: { priority: IssuePriority }) { +const STATUS_ICONS: Record = { + backlog: CircleDashed, + todo: Circle, + in_progress: CircleDot, + in_review: Eye, + done: CircleCheck, + blocked: CircleAlert, + cancelled: CircleX, +}; + +export function StatusIcon({ + status, + className = "h-4 w-4", +}: { + status: IssueStatus; + className?: string; +}) { + const Icon = STATUS_ICONS[status]; + const cfg = STATUS_CONFIG[status]; + return ; +} + +export function PriorityIcon({ + priority, + className = "", +}: { + priority: IssuePriority; + className?: string; +}) { const cfg = PRIORITY_CONFIG[priority]; + if (cfg.bars === 0) { + return ; + } return ( - - {cfg.shortLabel} - + + {[0, 1, 2, 3].map((i) => ( + + ))} + ); } -function AssigneeAvatar({ assignee }: { assignee: MockAssignee | null }) { +function AssigneeAvatar({ + assignee, + size = "sm", +}: { + assignee: MockAssignee | null; + size?: "sm" | "md"; +}) { if (!assignee) return null; + const sizeClass = size === "sm" ? "h-5 w-5 text-[10px]" : "h-6 w-6 text-xs"; return (
{assignee.type === "agent" ? ( - + ) : ( assignee.avatar.charAt(0) )} @@ -52,15 +111,11 @@ function AssigneeAvatar({ assignee }: { assignee: MockAssignee | null }) { ); } -function StatusDot({ status }: { status: IssueStatus }) { - const cfg = STATUS_CONFIG[status]; - return ; -} - -function formatDueDate(date: string | null): string | null { - if (!date) return null; - const d = new Date(date); - return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); +function formatDate(date: string): string { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); } // --------------------------------------------------------------------------- @@ -68,35 +123,29 @@ function formatDueDate(date: string | null): string | null { // --------------------------------------------------------------------------- function BoardCard({ issue }: { issue: MockIssue }) { - const due = formatDueDate(issue.dueDate); - const isOverdue = - issue.dueDate && new Date(issue.dueDate) < new Date() && issue.status !== "done"; - return ( -
- - {issue.key} +
+ + {issue.key}
-

{issue.title}

-
- - {due && ( - - - {due} - - )} - {issue.comments.length > 0 && ( - - {issue.comments.length} 💬 +

{issue.title}

+
+
+ + {issue.comments.length > 0 && ( + + + {issue.comments.length} + + )} +
+ {issue.dueDate && ( + + {formatDate(issue.dueDate)} )}
@@ -114,20 +163,18 @@ function BoardView() { ]; return ( -
+
{visibleStatuses.map((status) => { const cfg = STATUS_CONFIG[status]; const issues = MOCK_ISSUES.filter((i) => i.status === status); return ( -
- {/* Column header */} -
- - {cfg.label} +
+
+ + {cfg.label} {issues.length}
- {/* Cards */} -
+
{issues.map((issue) => ( ))} @@ -144,26 +191,18 @@ function BoardView() { // --------------------------------------------------------------------------- function ListRow({ issue }: { issue: MockIssue }) { - const due = formatDueDate(issue.dueDate); - const isOverdue = - issue.dueDate && new Date(issue.dueDate) < new Date() && issue.status !== "done"; - return ( - - {issue.key} + + {issue.key} + {issue.title} - {due && ( - - - {due} + {issue.dueDate && ( + + {formatDate(issue.dueDate)} )} @@ -172,7 +211,7 @@ function ListRow({ issue }: { issue: MockIssue }) { } function ListView() { - const visibleStatuses: IssueStatus[] = [ + const groupOrder: IssueStatus[] = [ "in_review", "in_progress", "todo", @@ -182,24 +221,20 @@ function ListView() { return (
- {visibleStatuses.map((status) => { + {groupOrder.map((status) => { const cfg = STATUS_CONFIG[status]; const issues = MOCK_ISSUES.filter((i) => i.status === status); if (issues.length === 0) return null; return (
- {/* Group header */} -
- - {cfg.label} +
+ + {cfg.label} {issues.length}
- {/* Rows */} -
- {issues.map((issue) => ( - - ))} -
+ {issues.map((issue) => ( + + ))}
); })} @@ -218,43 +253,41 @@ export default function IssuesPage() { return (
- {/* Header */} -
-
+ {/* Toolbar */} +
+

All Issues

- {/* View toggle */} -
+
-
- {/* Content */}
{view === "board" ? : }