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";...@@ -5,8 +5,10 @@ import { useEffect, type CSSProperties } from "react";
5import { useParams } from "@tanstack/react-router";5import { useParams } from "@tanstack/react-router";
6import { ThreadId } from "@t3tools/contracts";6import { ThreadId } from "@t3tools/contracts";
7import {7import {
8 CheckIcon,
8 CircleAlertIcon,9 CircleAlertIcon,
9 CircleCheckIcon,10 CircleCheckIcon,
11 CopyIcon,
10 InfoIcon,12 InfoIcon,
11 LoaderCircleIcon,13 LoaderCircleIcon,
12 TriangleAlertIcon,14 TriangleAlertIcon,
...@@ -14,6 +16,7 @@ import {...@@ -14,6 +16,7 @@ import {
1416
15import { cn } from "~/lib/utils";17import { cn } from "~/lib/utils";
16import { buttonVariants } from "~/components/ui/button";18import { buttonVariants } from "~/components/ui/button";
19import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
17import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic";20import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic";
1821
19type ThreadToastData = {22type ThreadToastData = {
...@@ -35,6 +38,25 @@ const TOAST_ICONS = {...@@ -35,6 +38,25 @@ const TOAST_ICONS = {
35 warning: TriangleAlertIcon,38 warning: TriangleAlertIcon,
36} as const;39} 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
38type ToastPosition =60type ToastPosition =
39 | "top-left"61 | "top-left"
40 | "top-center"62 | "top-center"
...@@ -284,12 +306,17 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -284,12 +306,17 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
284 )}306 )}
285307
286 <div className="flex min-w-0 flex-1 flex-col gap-0.5">308 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
287 <Toast.Title309 <div className="flex items-center justify-between gap-1">
288 className="min-w-0 break-words font-medium"310 <Toast.Title
289 data-slot="toast-title"311 className="min-w-0 break-words font-medium"
290 />312 data-slot="toast-title"
313 />
314 {toast.type === "error" && typeof toast.description === "string" && (
315 <CopyErrorButton text={toast.description} />
316 )}
317 </div>
291 <Toast.Description318 <Toast.Description
292 className="min-w-0 break-words text-muted-foreground"319 className="min-w-0 select-text break-words text-muted-foreground"
293 data-slot="toast-description"320 data-slot="toast-description"
294 />321 />
295 </div>322 </div>
...@@ -373,12 +400,17 @@ function AnchoredToasts() {...@@ -373,12 +400,17 @@ function AnchoredToasts() {
373 )}400 )}
374401
375 <div className="flex min-w-0 flex-1 flex-col gap-0.5">402 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
376 <Toast.Title403 <div className="flex items-center gap-1">
377 className="min-w-0 break-words font-medium"404 <Toast.Title
378 data-slot="toast-title"405 className="min-w-0 break-words font-medium"
379 />406 data-slot="toast-title"
407 />
408 {toast.type === "error" && typeof toast.description === "string" && (
409 <CopyErrorButton text={toast.description} />
410 )}
411 </div>
380 <Toast.Description412 <Toast.Description
381 className="min-w-0 break-words text-muted-foreground"413 className="min-w-0 select-text break-words text-muted-foreground"
382 data-slot="toast-description"414 data-slot="toast-description"
383 />415 />
384 </div>416 </div>