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:
parent
995f5b0b66
commit
49c7fb7fbd
2 changed files with 33 additions and 41 deletions
|
|
@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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)}`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue