diff --git a/apps/web/src/components/ui/toast.logic.test.ts b/apps/web/src/components/ui/toast.logic.test.ts new file mode 100644 index 000000000000..def62c884fef --- /dev/null +++ b/apps/web/src/components/ui/toast.logic.test.ts @@ -0,0 +1,16 @@ +import { assert, describe, it } from "vitest"; +import { shouldHideCollapsedToastContent } from "./toast.logic"; + +describe("shouldHideCollapsedToastContent", () => { + it("keeps a single visible toast readable", () => { + assert.equal(shouldHideCollapsedToastContent(0, 1), false); + }); + + it("keeps the front-most toast readable in a visible stack", () => { + assert.equal(shouldHideCollapsedToastContent(0, 3), false); + }); + + it("hides non-front toasts until the stack is expanded", () => { + assert.equal(shouldHideCollapsedToastContent(1, 3), true); + }); +}); diff --git a/apps/web/src/components/ui/toast.logic.ts b/apps/web/src/components/ui/toast.logic.ts new file mode 100644 index 000000000000..500203033abb --- /dev/null +++ b/apps/web/src/components/ui/toast.logic.ts @@ -0,0 +1,9 @@ +export function shouldHideCollapsedToastContent( + visibleToastIndex: number, + visibleToastCount: number, +): boolean { + // Keep the front-most toast readable even if Base UI marks it as "behind" + // due to toasts hidden by thread filtering. + if (visibleToastCount <= 1) return false; + return visibleToastIndex > 0; +} diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index 19f9bb6b6190..7b4879e9f029 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -13,6 +13,7 @@ import { import { cn } from "~/lib/utils"; import { buttonVariants } from "~/components/ui/button"; import { useStore } from "~/store"; +import { shouldHideCollapsedToastContent } from "./toast.logic"; type ThreadToastData = { threadId?: string | null; @@ -146,6 +147,9 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) { const { toasts } = Toast.useToastManager(); const activeThreadId = state.activeThreadId; const isTop = position.startsWith("top"); + const visibleToasts = toasts.filter((toast) => + shouldRenderForActiveThread(toast.data, activeThreadId), + ); useEffect(() => { const activeToastIds = new Set(toasts.map((toast) => toast.id)); @@ -172,100 +176,107 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) { data-position={position} data-slot="toast-viewport" > - {toasts - .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId)) - .map((toast) => { - const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; + {visibleToasts.map((toast, visibleIndex) => { + const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; + const hideCollapsedContent = shouldHideCollapsedToastContent( + visibleIndex, + visibleToasts.length, + ); - return ( - + + - - -
- {Icon && ( -
- -
- )} - -
- - -
-
- {toast.actionProps && ( - + {Icon && ( +
- {toast.actionProps.children} - + +
)} -
-
- ); - })} + +
+ + +
+ + {toast.actionProps && ( + + {toast.actionProps.children} + + )} + + + ); + })} );