Unify unread badge and archive X into single column on Mine tab

The unread dot and dismiss X now share the same rightmost column on
the Mine tab.  When an issue is unread the blue dot shows first;
clicking it marks the issue as read and reveals the X on hover for
archiving.  Read/unread state stays in sync across all inbox tabs.
Desktop dismiss animation polished with scale + slide.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dotta 2026-03-26 08:51:06 -05:00
parent 995f5b0b66
commit 49c7fb7fbd
2 changed files with 33 additions and 41 deletions

View file

@ -1,6 +1,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { Issue } from "@paperclipai/shared"; import type { Issue } from "@paperclipai/shared";
import { Link } from "@/lib/router"; import { Link } from "@/lib/router";
import { X } from "lucide-react";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { StatusIcon } from "./StatusIcon"; import { StatusIcon } from "./StatusIcon";
@ -17,6 +18,8 @@ interface IssueRowProps {
trailingMeta?: ReactNode; trailingMeta?: ReactNode;
unreadState?: UnreadState | null; unreadState?: UnreadState | null;
onMarkRead?: () => void; onMarkRead?: () => void;
onArchive?: () => void;
archiveDisabled?: boolean;
className?: string; className?: string;
} }
@ -31,6 +34,8 @@ export function IssueRow({
trailingMeta, trailingMeta,
unreadState = null, unreadState = null,
onMarkRead, onMarkRead,
onArchive,
archiveDisabled,
className, className,
}: IssueRowProps) { }: IssueRowProps) {
const issuePathId = issue.identifier ?? issue.id; const issuePathId = issue.identifier ?? issue.id;
@ -113,6 +118,26 @@ export function IssueRow({
)} )}
/> />
</button> </button>
) : onArchive ? (
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onArchive();
}}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
event.stopPropagation();
onArchive();
}}
disabled={archiveDisabled}
className="inline-flex h-4 w-4 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 disabled:pointer-events-none disabled:opacity-30"
aria-label="Dismiss from inbox"
>
<X className="h-3.5 w-3.5" />
</button>
) : ( ) : (
<span className="inline-flex h-4 w-4" aria-hidden="true" /> <span className="inline-flex h-4 w-4" aria-hidden="true" />
)} )}

View file

@ -94,35 +94,6 @@ function readIssueIdFromRun(run: HeartbeatRun): string | null {
return null; return null;
} }
function InboxArchiveButton({
onArchive,
disabled,
}: {
onArchive: () => void;
disabled: boolean;
}) {
return (
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onArchive();
}}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
event.stopPropagation();
onArchive();
}}
disabled={disabled}
className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100 disabled:pointer-events-none disabled:opacity-30"
aria-label="Archive from mine"
>
<X className="h-4 w-4" />
</button>
);
}
function FailedRunInboxRow({ function FailedRunInboxRow({
run, run,
@ -957,8 +928,8 @@ export function Inbox() {
issueLinkState={issueLinkState} issueLinkState={issueLinkState}
className={ className={
isArchiving isArchiving
? "pointer-events-none -translate-x-3 opacity-0 transition-transform transition-opacity duration-200" ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
: "transition-transform transition-opacity duration-200" : "transition-all duration-200 ease-out"
} }
desktopMetaLeading={( desktopMetaLeading={(
<> <>
@ -987,19 +958,15 @@ export function Inbox() {
: `updated ${timeAgo(issue.updatedAt)}` : `updated ${timeAgo(issue.updatedAt)}`
} }
unreadState={ unreadState={
isMineTab isUnread ? "visible" : isFading ? "fading" : "hidden"
? null
: isUnread ? "visible" : isFading ? "fading" : "hidden"
} }
onMarkRead={() => markReadMutation.mutate(issue.id)} onMarkRead={() => markReadMutation.mutate(issue.id)}
desktopTrailing={ onArchive={
isMineTab ? ( isMineTab
<InboxArchiveButton ? () => archiveIssueMutation.mutate(issue.id)
onArchive={() => archiveIssueMutation.mutate(issue.id)} : undefined
disabled={isArchiving || archiveIssueMutation.isPending}
/>
) : undefined
} }
archiveDisabled={isArchiving || archiveIssueMutation.isPending}
trailingMeta={ trailingMeta={
issue.lastExternalCommentAt issue.lastExternalCommentAt
? `commented ${timeAgo(issue.lastExternalCommentAt)}` ? `commented ${timeAgo(issue.lastExternalCommentAt)}`