| ... | @@ -5,8 +5,10 @@ import { useEffect, type CSSProperties } from "react"; | ... | @@ -5,8 +5,10 @@ import { useEffect, type CSSProperties } from "react"; |
| 5 | import { useParams } from "@tanstack/react-router"; | 5 | import { useParams } from "@tanstack/react-router"; |
| 6 | import { ThreadId } from "@t3tools/contracts"; | 6 | import { ThreadId } from "@t3tools/contracts"; |
| 7 | import { | 7 | import { |
| | 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 { |
| 14 | | 16 | |
| 15 | import { cn } from "~/lib/utils"; | 17 | import { cn } from "~/lib/utils"; |
| 16 | import { buttonVariants } from "~/components/ui/button"; | 18 | import { buttonVariants } from "~/components/ui/button"; |
| | 19 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 17 | import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic"; | 20 | import { buildVisibleToastLayout, shouldHideCollapsedToastContent } from "./toast.logic"; |
| 18 | | 21 | |
| 19 | type ThreadToastData = { | 22 | type 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; |
| 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 | type ToastPosition = | 60 | type 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 | )} |
| 285 | | 307 | |
| 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.Title | 309 | <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.Description | 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 | 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 | )} |
| 374 | | 401 | |
| 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.Title | 403 | <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.Description | 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 | data-slot="toast-description" | 414 | data-slot="toast-description" |
| 383 | /> | 415 | /> |
| 384 | </div> | 416 | </div> |