Unify issue rows with GitHub-style mobile layout across app
On mobile, all issue rows now show title first (up to 2 lines), with metadata (icons, identifier, timestamp) on a second line below. Desktop layout is preserved as single-line rows. Affected locations: Inbox recent issues, Inbox stale work, Dashboard recent tasks, and IssuesList. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -608,22 +608,45 @@ export function IssuesList({
|
|||||||
<Link
|
<Link
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
className="flex items-center gap-2 py-2 pl-1 pr-3 text-sm border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/50 transition-colors no-underline text-inherit"
|
className="flex flex-col gap-1 py-2.5 pl-2 pr-3 text-sm border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/50 transition-colors no-underline text-inherit sm:flex-row sm:items-center sm:gap-2 sm:py-2 sm:pl-1"
|
||||||
>
|
>
|
||||||
|
{/* Title line - first on mobile, middle on desktop */}
|
||||||
|
<span className="line-clamp-2 text-sm pl-1 sm:order-2 sm:flex-1 sm:min-w-0 sm:pl-0 sm:line-clamp-none sm:truncate">
|
||||||
|
{issue.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Metadata line - second on mobile, first on desktop */}
|
||||||
|
<span className="flex items-center gap-2 sm:order-1 sm:shrink-0">
|
||||||
{/* Spacer matching caret width so status icon aligns with group title (hidden on mobile) */}
|
{/* Spacer matching caret width so status icon aligns with group title (hidden on mobile) */}
|
||||||
<div className="w-3.5 shrink-0 hidden sm:block" />
|
<span className="w-3.5 shrink-0 hidden sm:block" />
|
||||||
<div className="shrink-0" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
|
<span className="shrink-0" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
|
||||||
<StatusIcon
|
<StatusIcon
|
||||||
status={issue.status}
|
status={issue.status}
|
||||||
onChange={(s) => onUpdateIssue(issue.id, { status: s })}
|
onChange={(s) => onUpdateIssue(issue.id, { status: s })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground font-mono shrink-0">
|
<span className="text-sm text-muted-foreground font-mono shrink-0">
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate flex-1 min-w-0">{issue.title}</span>
|
{liveIssueIds?.has(issue.id) && (
|
||||||
|
<span className="inline-flex items-center gap-1 sm:gap-1.5 px-1.5 sm:px-2 py-0.5 rounded-full bg-blue-500/10">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400 hidden sm:inline">Live</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-muted-foreground sm:hidden">·</span>
|
||||||
|
<span className="text-xs text-muted-foreground sm:hidden">
|
||||||
|
{formatDate(issue.createdAt)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Desktop-only trailing content */}
|
||||||
|
<span className="hidden sm:flex sm:order-3 items-center gap-2 sm:gap-3 shrink-0 ml-auto">
|
||||||
{(issue.labels ?? []).length > 0 && (
|
{(issue.labels ?? []).length > 0 && (
|
||||||
<div className="hidden md:flex items-center gap-1 max-w-[240px] overflow-hidden">
|
<span className="hidden md:flex items-center gap-1 max-w-[240px] overflow-hidden">
|
||||||
{(issue.labels ?? []).slice(0, 3).map((label) => (
|
{(issue.labels ?? []).slice(0, 3).map((label) => (
|
||||||
<span
|
<span
|
||||||
key={label.id}
|
key={label.id}
|
||||||
@@ -640,19 +663,8 @@ export function IssuesList({
|
|||||||
{(issue.labels ?? []).length > 3 && (
|
{(issue.labels ?? []).length > 3 && (
|
||||||
<span className="text-[10px] text-muted-foreground">+{(issue.labels ?? []).length - 3}</span>
|
<span className="text-[10px] text-muted-foreground">+{(issue.labels ?? []).length - 3}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2 sm:gap-3 shrink-0 ml-auto">
|
|
||||||
{liveIssueIds?.has(issue.id) && (
|
|
||||||
<span className="inline-flex items-center gap-1 sm:gap-1.5 px-1.5 sm:px-2 py-0.5 rounded-full bg-blue-500/10">
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
|
||||||
</span>
|
|
||||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400 hidden sm:inline">Live</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="hidden sm:block">
|
|
||||||
<Popover
|
<Popover
|
||||||
open={assigneePickerIssueId === issue.id}
|
open={assigneePickerIssueId === issue.id}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@@ -731,11 +743,10 @@ export function IssuesList({
|
|||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
<span className="text-xs text-muted-foreground hidden sm:inline">
|
|
||||||
{formatDate(issue.createdAt)}
|
{formatDate(issue.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|||||||
@@ -313,27 +313,29 @@ export function Dashboard() {
|
|||||||
<Link
|
<Link
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
className="px-4 py-2 text-sm cursor-pointer hover:bg-accent/50 transition-colors no-underline text-inherit block"
|
className="px-4 py-2.5 text-sm cursor-pointer hover:bg-accent/50 transition-colors no-underline text-inherit block"
|
||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
|
||||||
<div className="flex items-start gap-2 min-w-0 flex-1">
|
<span className="line-clamp-2 text-sm sm:order-2 sm:flex-1 sm:min-w-0 sm:line-clamp-none sm:truncate">
|
||||||
<div className="flex items-center gap-2 shrink-0 mt-0.5">
|
{issue.title}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2 sm:order-1 sm:shrink-0">
|
||||||
<PriorityIcon priority={issue.priority} />
|
<PriorityIcon priority={issue.priority} />
|
||||||
<StatusIcon status={issue.status} />
|
<StatusIcon status={issue.status} />
|
||||||
</div>
|
<span className="text-xs font-mono text-muted-foreground">
|
||||||
<p className="min-w-0 flex-1 truncate">
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
<span>{issue.title}</span>
|
</span>
|
||||||
{issue.assigneeAgentId && (() => {
|
{issue.assigneeAgentId && (() => {
|
||||||
const name = agentName(issue.assigneeAgentId);
|
const name = agentName(issue.assigneeAgentId);
|
||||||
return name
|
return name
|
||||||
? <span className="hidden sm:inline"><Identity name={name} size="sm" className="ml-2 inline-flex" /></span>
|
? <span className="hidden sm:inline-flex"><Identity name={name} size="sm" /></span>
|
||||||
: null;
|
: null;
|
||||||
})()}
|
})()}
|
||||||
</p>
|
<span className="text-xs text-muted-foreground sm:hidden">·</span>
|
||||||
</div>
|
<span className="text-xs text-muted-foreground shrink-0 sm:order-last">
|
||||||
<span className="text-xs text-muted-foreground shrink-0 pt-0.5">
|
|
||||||
{timeAgo(issue.updatedAt)}
|
{timeAgo(issue.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -841,38 +841,39 @@ export function Inbox() {
|
|||||||
{staleIssues.map((issue) => (
|
{staleIssues.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="group/stale relative flex items-center gap-3 overflow-hidden px-4 py-3 transition-colors hover:bg-accent/50"
|
className="group/stale relative flex items-start gap-3 overflow-hidden px-4 py-3 transition-colors hover:bg-accent/50"
|
||||||
>
|
>
|
||||||
|
<Clock className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground sm:mt-0" />
|
||||||
<Link
|
<Link
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
|
className="flex min-w-0 flex-1 cursor-pointer flex-col gap-1 no-underline text-inherit sm:flex-row sm:items-center sm:gap-3"
|
||||||
>
|
>
|
||||||
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<span className="line-clamp-2 text-sm sm:order-2 sm:flex-1 sm:min-w-0 sm:line-clamp-none sm:truncate">
|
||||||
|
{issue.title}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2 sm:order-1 sm:shrink-0">
|
||||||
<PriorityIcon priority={issue.priority} />
|
<PriorityIcon priority={issue.priority} />
|
||||||
<StatusIcon status={issue.status} />
|
<StatusIcon status={issue.status} />
|
||||||
<span className="shrink-0 text-xs font-mono text-muted-foreground">
|
<span className="shrink-0 text-xs font-mono text-muted-foreground">
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate text-sm">{issue.title}</span>
|
|
||||||
{issue.assigneeAgentId &&
|
{issue.assigneeAgentId &&
|
||||||
(() => {
|
(() => {
|
||||||
const name = agentName(issue.assigneeAgentId);
|
const name = agentName(issue.assigneeAgentId);
|
||||||
return name ? (
|
return name ? (
|
||||||
<Identity name={name} size="sm" />
|
<span className="hidden sm:inline-flex"><Identity name={name} size="sm" /></span>
|
||||||
) : (
|
) : null;
|
||||||
<span className="shrink-0 font-mono text-xs text-muted-foreground">
|
|
||||||
{issue.assigneeAgentId.slice(0, 8)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})()}
|
})()}
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground sm:hidden">·</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground sm:order-last">
|
||||||
updated {timeAgo(issue.updatedAt)}
|
updated {timeAgo(issue.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dismiss(`stale:${issue.id}`)}
|
onClick={() => dismiss(`stale:${issue.id}`)}
|
||||||
className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/stale:opacity-100"
|
className="mt-0.5 rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/stale:opacity-100 sm:mt-0"
|
||||||
aria-label="Dismiss"
|
aria-label="Dismiss"
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
@@ -900,7 +901,7 @@ export function Inbox() {
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
||||||
>
|
>
|
||||||
<span className="flex h-5 w-4 shrink-0 items-center justify-center">
|
<span className="flex h-5 w-4 shrink-0 items-center justify-center mt-0.5 sm:mt-0">
|
||||||
{(isUnread || isFading) && (
|
{(isUnread || isFading) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -922,20 +923,25 @@ export function Inbox() {
|
|||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
to={`/issues/${issue.identifier ?? issue.id}`}
|
||||||
className="flex flex-1 min-w-0 cursor-pointer flex-wrap items-center gap-x-3 gap-y-0.5 no-underline text-inherit sm:flex-nowrap"
|
className="flex flex-1 min-w-0 cursor-pointer flex-col gap-1 no-underline text-inherit sm:flex-row sm:items-center sm:gap-3"
|
||||||
>
|
>
|
||||||
|
<span className="line-clamp-2 text-sm sm:order-2 sm:flex-1 sm:min-w-0 sm:line-clamp-none sm:truncate">
|
||||||
|
{issue.title}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2 sm:order-1 sm:shrink-0">
|
||||||
<PriorityIcon priority={issue.priority} />
|
<PriorityIcon priority={issue.priority} />
|
||||||
<StatusIcon status={issue.status} />
|
<StatusIcon status={issue.status} />
|
||||||
<span className="text-xs font-mono text-muted-foreground">
|
<span className="text-xs font-mono text-muted-foreground">
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto shrink-0 text-xs text-muted-foreground sm:order-last">
|
<span className="text-xs text-muted-foreground sm:hidden">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground sm:order-last">
|
||||||
{issue.lastExternalCommentAt
|
{issue.lastExternalCommentAt
|
||||||
? `commented ${timeAgo(issue.lastExternalCommentAt)}`
|
? `commented ${timeAgo(issue.lastExternalCommentAt)}`
|
||||||
: `updated ${timeAgo(issue.updatedAt)}`}
|
: `updated ${timeAgo(issue.updatedAt)}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-full line-clamp-2 text-sm sm:w-0 sm:flex-1 sm:line-clamp-none sm:truncate">
|
|
||||||
{issue.title}
|
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user