| ... | ... | @@ -5,8 +5,10 @@ import { useEffect, type CSSProperties } from "react"; |
| 5 | 5 | import { useParams } from "@tanstack/react-router"; |
| 6 | 6 | import { ThreadId } from "@t3tools/contracts"; |
| 7 | 7 | import { |
| 8 | CheckIcon, |
| 8 | 9 | CircleAlertIcon, |
| 9 | 10 | CircleCheckIcon, |
| 11 | CopyIcon, |
| 10 | 12 | InfoIcon, |
| 11 | 13 | LoaderCircleIcon, |
| 12 | 14 | TriangleAlertIcon, |
| ... | ... | @@ -14,6 +16,7 @@ import { |
| 14 | 16 | |
| 15 | 17 | import { cn } from "~/lib/utils"; |
| 16 | 18 | import { buttonVariants } from "~/components/ui/button"; |
| 19 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 17 | 20 | import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic"; |
| 18 | 21 | |
| 19 | 22 | type ThreadToastData = { |
| ... | ... | @@ -35,6 +38,25 @@ const TOAST_ICONS = { |
| 35 | 38 | warning: TriangleAlertIcon, |
| 36 | 39 | } as const; |
| 37 | 40 | |
| 41 | function 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 | |
| 38 | 60 | type ToastPosition = |
| 39 | 61 | | "top-left" |
| 40 | 62 | | "top-center" |
| ... | ... | @@ -284,12 +306,17 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) { |
| 284 | 306 | )} |
| 285 | 307 | |
| 286 | 308 | <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> |
| 291 | 318 | <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 | 320 | data-slot="toast-description" |
| 294 | 321 | /> |
| 295 | 322 | </div> |
| ... | ... | @@ -373,12 +400,17 @@ function AnchoredToasts() { |
| 373 | 400 | )} |
| 374 | 401 | |
| 375 | 402 | <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> |
| 380 | 412 | <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 | 414 | data-slot="toast-description" |
| 383 | 415 | /> |
| 384 | 416 | </div> |