authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 01:15:29-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 14:43:19-08:00
log95fcc89074d44518cded3adfa08dc18284c87fbe
treef789e69c8ebe16bc054b1e04ff9f194125a08741
parenteb59853596b9ea2547c1bde87d2cd9b59a6fef95

Prevent toast content overflow in collapsed stack

- Use the larger of shared stack height and per-toast height to avoid clipping - Make toast content columns flexible with `min-w-0` and `break-words` - Keep action buttons from shrinking so long text wraps instead of overflowing

1 files changed, 23 insertions(+), 11 deletions(-)

apps/web/src/components/ui/toast.tsx+23-11
...@@ -206,7 +206,10 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -206,7 +206,10 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
206 "data-[position*=top]:after:top-full",206 "data-[position*=top]:after:top-full",
207 "data-[position*=bottom]:after:bottom-full",207 "data-[position*=bottom]:after:bottom-full",
208 // Define some variables208 // Define some variables
209 "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",209 // Base UI exposes a shared front-most height for the collapsed stack.
210 // If that shared measurement is briefly stale, long content can render
211 // outside the card until hover expands the toast and swaps to its own height.
212 "[--toast-calc-height:max(var(--toast-frontmost-height,var(--toast-height)),var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
210 // Define offset-y variable213 // Define offset-y variable
211 "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",214 "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
212 "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",215 "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
...@@ -258,7 +261,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -258,7 +261,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
258 "not-data-expanded:pointer-events-none not-data-expanded:opacity-0",261 "not-data-expanded:pointer-events-none not-data-expanded:opacity-0",
259 )}262 )}
260 >263 >
261 <div className="flex gap-2">264 <div className="flex min-w-0 flex-1 gap-2">
262 {Icon && (265 {Icon && (
263 <div266 <div
264 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"267 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
...@@ -268,16 +271,22 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -268,16 +271,22 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
268 </div>271 </div>
269 )}272 )}
270273
271 <div className="flex flex-col gap-0.5">274 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
272 <Toast.Title className="font-medium" data-slot="toast-title" />275 <Toast.Title
276 className="min-w-0 break-words font-medium"
277 data-slot="toast-title"
278 />
273 <Toast.Description279 <Toast.Description
274 className="text-muted-foreground"280 className="min-w-0 break-words text-muted-foreground"
275 data-slot="toast-description"281 data-slot="toast-description"
276 />282 />
277 </div>283 </div>
278 </div>284 </div>
279 {toast.actionProps && (285 {toast.actionProps && (
280 <Toast.Action className={buttonVariants({ size: "xs" })} data-slot="toast-action">286 <Toast.Action
287 className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
288 data-slot="toast-action"
289 >
281 {toast.actionProps.children}290 {toast.actionProps.children}
282 </Toast.Action>291 </Toast.Action>
283 )}292 )}
...@@ -341,7 +350,7 @@ function AnchoredToasts() {...@@ -341,7 +350,7 @@ function AnchoredToasts() {
341 </Toast.Content>350 </Toast.Content>
342 ) : (351 ) : (
343 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">352 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
344 <div className="flex gap-2">353 <div className="flex min-w-0 flex-1 gap-2">
345 {Icon && (354 {Icon && (
346 <div355 <div
347 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"356 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
...@@ -351,17 +360,20 @@ function AnchoredToasts() {...@@ -351,17 +360,20 @@ function AnchoredToasts() {
351 </div>360 </div>
352 )}361 )}
353362
354 <div className="flex flex-col gap-0.5">363 <div className="flex min-w-0 flex-1 flex-col gap-0.5">
355 <Toast.Title className="font-medium" data-slot="toast-title" />364 <Toast.Title
365 className="min-w-0 break-words font-medium"
366 data-slot="toast-title"
367 />
356 <Toast.Description368 <Toast.Description
357 className="text-muted-foreground"369 className="min-w-0 break-words text-muted-foreground"
358 data-slot="toast-description"370 data-slot="toast-description"
359 />371 />
360 </div>372 </div>
361 </div>373 </div>
362 {toast.actionProps && (374 {toast.actionProps && (
363 <Toast.Action375 <Toast.Action
364 className={buttonVariants({ size: "xs" })}376 className={cn(buttonVariants({ size: "xs" }), "shrink-0")}
365 data-slot="toast-action"377 data-slot="toast-action"
366 >378 >
367 {toast.actionProps.children}379 {toast.actionProps.children}