authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-25 13:01:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-25 13:01:04-07:00
log8888f6a0c9ad2dfdce6b97d65c0cc08e889aad79
tree437f297cd8c8f14c74e790e04c824abd23751550
parentff8d87ddee289d58c4b17281a74d84efe750cfdc
signature Signed by PGP key B5690EEEBB952194

Add copy action to error toast messages (#1416)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>

1 files changed, 42 insertions(+), 10 deletions(-)

apps/web/src/components/ui/toast.tsx+42-10
......@@ -5,8 +5,10 @@ import { useEffect, type CSSProperties } from "react";
55import { useParams } from "@tanstack/react-router";
66import { ThreadId } from "@t3tools/contracts";
77import {
8 CheckIcon,
89 CircleAlertIcon,
910 CircleCheckIcon,
11 CopyIcon,
1012 InfoIcon,
1113 LoaderCircleIcon,
1214 TriangleAlertIcon,
......@@ -14,6 +16,7 @@ import {
1416
1517import { cn } from "~/lib/utils";
1618import { buttonVariants } from "~/components/ui/button";
19import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
1720import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic";
1821
1922type ThreadToastData = {
......@@ -35,6 +38,25 @@ const TOAST_ICONS = {
3538 warning: TriangleAlertIcon,
3639} as const;
3740
41function CopyErrorButton({ text }: { text: string }) {
42 const { copyToClipboard, isCopied } = useCopyToClipboard();
43
44 return (
45 <button
46 className="shrink-0 cursor-pointer rounded-md p-1 text-muted-foreground opacity-60 transition-opacity hover:opacity-100"
47 onClick={() => copyToClipboard(text)}
48 title="Copy error"
49 type="button"
50 >
51 {isCopied ? (
52 <CheckIcon className="size-3.5 text-success" />
53 ) : (
54 <CopyIcon className="size-3.5" />
55 )}
56 </button>
57 );
58}
59
3860type ToastPosition =
3961 | "top-left"
4062 | "top-center"
......@@ -284,12 +306,17 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
284306 )}
285307
286308 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
287 <Toast.Title
288 className="min-w-0 break-words font-medium"
289 data-slot="toast-title"
290 />
309 <div className="flex items-center justify-between gap-1">
310 <Toast.Title
311 className="min-w-0 break-words font-medium"
312 data-slot="toast-title"
313 />
314 {toast.type === "error" && typeof toast.description === "string" && (
315 <CopyErrorButton text={toast.description} />
316 )}
317 </div>
291318 <Toast.Description
292 className="min-w-0 break-words text-muted-foreground"
319 className="min-w-0 select-text break-words text-muted-foreground"
293320 data-slot="toast-description"
294321 />
295322 </div>
......@@ -373,12 +400,17 @@ function AnchoredToasts() {
373400 )}
374401
375402 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
376 <Toast.Title
377 className="min-w-0 break-words font-medium"
378 data-slot="toast-title"
379 />
403 <div className="flex items-center gap-1">
404 <Toast.Title
405 className="min-w-0 break-words font-medium"
406 data-slot="toast-title"
407 />
408 {toast.type === "error" && typeof toast.description === "string" && (
409 <CopyErrorButton text={toast.description} />
410 )}
411 </div>
380412 <Toast.Description
381 className="min-w-0 break-words text-muted-foreground"
413 className="min-w-0 select-text break-words text-muted-foreground"
382414 data-slot="toast-description"
383415 />
384416 </div>