From 9bcd91af393a39443827efdf3e11a5b77e0a5182 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 17:31:27 -0400 Subject: [PATCH 01/15] Remove unused animations --- frontend/src/styles/tailwind.css | 33 -------------------------------- 1 file changed, 33 deletions(-) diff --git a/frontend/src/styles/tailwind.css b/frontend/src/styles/tailwind.css index 303278c98..46be6567d 100644 --- a/frontend/src/styles/tailwind.css +++ b/frontend/src/styles/tailwind.css @@ -131,23 +131,10 @@ --animate-slideLeftAndFadeOut: slideLeftAndFadeOut 400ms cubic-bezier(0.16, 1, 0.3, 1); - --animate-slideUp: slideUp 400ms cubic-bezier(0.16, 1, 0.3, 1); - --animate-slideDown: slideDown 400ms cubic-bezier(0.16, 1, 0.3, 1); - --animate-hide: hide 100ms ease-in; --animate-slideIn: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); --animate-slideOut: slideOut 400ms cubic-bezier(0.16, 1, 0.3, 1); --animate-swipeOut: swipeOut 100ms ease-out; - @keyframes hide { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } - } - @keyframes slideIn { 0% { transform: translateX(calc(100% + var(--viewport-padding))); @@ -275,26 +262,6 @@ } } - @keyframes slideUp { - 0% { - transform: translateY(100%); - } - - 100% { - transform: translateY(0); - } - } - - @keyframes slideDown { - 0% { - transform: translateY(0); - } - - 100% { - transform: translateY(100%); - } - } - /* Faster duration, much smaller degree */ --animate-wiggle: wiggle 0.35s linear infinite; From 0c8c7c86800b48ab38cd9ae5d41b19e73795a98e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 17:34:44 -0400 Subject: [PATCH 02/15] Rename toast animations --- .../src/features/system-feedback/toast/base.tsx | 2 +- frontend/src/styles/tailwind.css | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index ece3bf848..ba20c419a 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -65,7 +65,7 @@ export default function BaseToast({ Date: Tue, 30 Jun 2026 17:36:18 -0400 Subject: [PATCH 03/15] Reorganize animations --- .../features/system-feedback/toast/base.tsx | 4 +- frontend/src/styles/tailwind.css | 92 +++++++++---------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index ba20c419a..6042164ed 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -65,7 +65,9 @@ export default function BaseToast({ Date: Tue, 30 Jun 2026 17:44:47 -0400 Subject: [PATCH 04/15] Simplify toast structure --- .../features/system-feedback/toast/base.tsx | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 6042164ed..090a3a27e 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -64,7 +64,7 @@ export default function BaseToast({ return ( )} -
{icon}
+
+
+
{icon}
-
- - {title} - - - {message} - -
+
+ + {title} + + + {message} + +
+
- - - + + + +
); } From b8c56439745a919ab64d43984e3cb262e0ce686f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 17:52:25 -0400 Subject: [PATCH 05/15] Move mobile toasts to viewport top --- .../system-feedback/toast/provider.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index 7e15e9ea3..c4e0d6a09 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -9,6 +9,7 @@ import { TOAST_CONFIG } from "@/features/system-feedback/toast/config"; import ToastContext from "@/features/system-feedback/toast/context"; import { ToastData, ToastOptions } from "@/features/system-feedback/toast/type"; import { ToastType } from "@/features/system-feedback/type"; +import useCheckMobile from "@/lib/hooks/use-check-mobile"; import { cn } from "@/lib/utils/classname"; export default function ToastProvider({ @@ -16,6 +17,8 @@ export default function ToastProvider({ }: { children: React.ReactNode; }) { + const isMobile = useCheckMobile(); + const [toasts, setToasts] = useState([]); const [isHoveringViewport, setIsHoveringViewport] = useState(false); @@ -110,14 +113,24 @@ export default function ToastProvider({ onMouseLeave={() => setIsHoveringViewport(false)} style={{ // adjust the position of the toast viewport based on keyboard height - transform: `translateY(-${keyboardHeight}px)`, + transform: isMobile + ? undefined + : `translateY(-${keyboardHeight}px)`, transition: "transform 0.2s ease-out", }} className={cn( - "fixed bottom-12 right-0 z-[2147483647] md:bottom-0", - keyboardHeight > 0 && "bottom-0", - "flex list-none flex-col items-end outline-none", - "m-0 space-y-1 pb-[var(--viewport-padding)] pr-[var(--viewport-padding)] [--viewport-padding:_25px]", + "fixed z-[2147483647]", + "m-0 list-none outline-none [--viewport-padding:_25px]", + isMobile + ? cn( + "left-0 right-0 top-0", + "px-[var(--viewport-padding)] pt-[var(--viewport-padding)]", + ) + : cn( + "bottom-0 right-0", + "flex flex-col items-end", + "space-y-1 pb-[var(--viewport-padding)] pr-[var(--viewport-padding)]", + ), )} > {toasts.map((toast) => { From e25eea57db3d2667806e82bc41c60e77d1d60fd0 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 20:45:47 -0400 Subject: [PATCH 06/15] Overhaul mobile toast layout --- .../features/system-feedback/toast/base.tsx | 29 +++++++++++++++++-- .../system-feedback/toast/provider.tsx | 5 ++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 090a3a27e..d9155776e 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -4,6 +4,7 @@ import * as Toast from "@radix-ui/react-toast"; import { XIcon } from "lucide-react"; import ProgressBar from "@/features/system-feedback/toast/progress-bar"; +import useCheckMobile from "@/lib/hooks/use-check-mobile"; import { cn } from "@/lib/utils/classname"; type BaseToastProps = { @@ -17,6 +18,7 @@ type BaseToastProps = { duration?: number; isPersistent?: boolean; isPaused: boolean; + index: number; }; export default function BaseToast({ @@ -30,6 +32,7 @@ export default function BaseToast({ duration = 5000, isPersistent = false, isPaused, + index, }: BaseToastProps) { // Whenever the viewport hover state changes, the toast provider rerenders // all toasts, which means that all toast durations will get reset. In order @@ -61,10 +64,20 @@ export default function BaseToast({ }; }, [isPaused, open, isPersistent, onOpenChange]); + const isMobile = useCheckMobile(); + return ( )} -
+
1 && "opacity-0", + )} + >
{icon}
diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index c4e0d6a09..462a3b2d3 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -133,7 +133,7 @@ export default function ToastProvider({ ), )} > - {toasts.map((toast) => { + {toasts.map((toast, index) => { const config = TOAST_CONFIG[toast.type] || TOAST_CONFIG.info; const Icon = config.icon; @@ -148,7 +148,7 @@ export default function ToastProvider({ icon={} isPersistent={toast.isPersistent} duration={toast.duration} - isPaused={isHoveringViewport} + isPaused={isHoveringViewport || (isMobile && index > 0)} onOpenChange={(isOpen) => { if (!isOpen) { if (toast.onDismiss) { @@ -157,6 +157,7 @@ export default function ToastProvider({ removeToast(toast.id); } }} + index={index} /> ); })} From 786c1a4c42e9625e03149be039b11eb9a57e1d62 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 21:00:17 -0400 Subject: [PATCH 07/15] Fix toast content display on desktop --- frontend/src/features/system-feedback/toast/base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index d9155776e..f9534e0bc 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -108,7 +108,7 @@ export default function BaseToast({
1 && "opacity-0", + isMobile && index > 1 && "opacity-0", )} >
From cef2a717ef2c916443f3d7a676b273945d6c7dff Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 21:03:21 -0400 Subject: [PATCH 08/15] Change mobile toast animation to vertical --- .../features/system-feedback/toast/base.tsx | 13 ++++-- .../system-feedback/toast/provider.tsx | 2 +- frontend/src/styles/tailwind.css | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index f9534e0bc..202d4428d 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -76,11 +76,16 @@ export default function BaseToast({ "transition-[transform,background-color,scale] duration-200 ease-out", "origin-[center_-100%]", index < 3 && "shadow-xl", + "data-[state=closed]:animate-toastSlideOutUp data-[state=open]:animate-toastSlideInDown", + "data-[swipe=end]:animate-toastSwipeOutUp data-[swipe=move]:translate-y-[var(--radix-toast-swipe-move-y)]", + "data-[swipe=cancel]:translate-y-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out]", ) - : cn("relative shadow-xl"), - "data-[state=closed]:animate-toastSlideOutRight data-[state=open]:animate-toastSlideInLeft", - "data-[swipe=end]:animate-toastSwipeOutRight data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]", - "data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out]", + : cn( + "relative shadow-xl", + "data-[state=closed]:animate-toastSlideOutRight data-[state=open]:animate-toastSlideInLeft", + "data-[swipe=end]:animate-toastSwipeOutRight data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]", + "data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out]", + ), )} style={{ backgroundColor: `var(--color-${backgroundColor})`, diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index 462a3b2d3..fc61d2c3e 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -105,7 +105,7 @@ export default function ToastProvider({ return ( - + {children} Date: Tue, 30 Jun 2026 21:11:04 -0400 Subject: [PATCH 09/15] Reverse mobile toast order --- .../src/features/system-feedback/toast/base.tsx | 14 +++++++------- .../features/system-feedback/toast/provider.tsx | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 202d4428d..12361a821 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -18,7 +18,7 @@ type BaseToastProps = { duration?: number; isPersistent?: boolean; isPaused: boolean; - index: number; + stackIndex: number; }; export default function BaseToast({ @@ -32,7 +32,7 @@ export default function BaseToast({ duration = 5000, isPersistent = false, isPaused, - index, + stackIndex, }: BaseToastProps) { // Whenever the viewport hover state changes, the toast provider rerenders // all toasts, which means that all toast durations will get reset. In order @@ -75,7 +75,7 @@ export default function BaseToast({ "absolute w-[calc(100%-2*var(--viewport-padding))]", "transition-[transform,background-color,scale] duration-200 ease-out", "origin-[center_-100%]", - index < 3 && "shadow-xl", + stackIndex < 3 && "shadow-xl", "data-[state=closed]:animate-toastSlideOutUp data-[state=open]:animate-toastSlideInDown", "data-[swipe=end]:animate-toastSwipeOutUp data-[swipe=move]:translate-y-[var(--radix-toast-swipe-move-y)]", "data-[swipe=cancel]:translate-y-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out]", @@ -92,9 +92,9 @@ export default function BaseToast({ color: `var(--color-${textColor})`, ...(isMobile ? { - zIndex: 2147483647 - index, - scale: Math.max(0, 1 - index * 0.1), - backgroundColor: `color-mix(in oklab, var(--color-${backgroundColor}), var(--color-background) ${index * 40}%)`, + zIndex: 2147483647 - stackIndex, + scale: Math.max(0, 1 - stackIndex * 0.1), + backgroundColor: `color-mix(in oklab, var(--color-${backgroundColor}), var(--color-background) ${stackIndex * 40}%)`, } : {}), }} @@ -113,7 +113,7 @@ export default function BaseToast({
1 && "opacity-0", + isMobile && stackIndex > 1 && "opacity-0", )} >
diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index fc61d2c3e..ca24aaef4 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -136,6 +136,7 @@ export default function ToastProvider({ {toasts.map((toast, index) => { const config = TOAST_CONFIG[toast.type] || TOAST_CONFIG.info; const Icon = config.icon; + const stackIndex = toasts.length - index - 1; return ( } isPersistent={toast.isPersistent} duration={toast.duration} - isPaused={isHoveringViewport || (isMobile && index > 0)} + isPaused={isHoveringViewport || (isMobile && stackIndex > 0)} onOpenChange={(isOpen) => { if (!isOpen) { if (toast.onDismiss) { @@ -157,7 +158,7 @@ export default function ToastProvider({ removeToast(toast.id); } }} - index={index} + stackIndex={stackIndex} /> ); })} From c79ee1e430b49a3bf7fc9e1e740b87553774f469 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 30 Jun 2026 21:29:15 -0400 Subject: [PATCH 10/15] Add toast duration reset on mobile --- .../src/features/system-feedback/toast/base.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 12361a821..7bdc45081 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import * as Toast from "@radix-ui/react-toast"; import { XIcon } from "lucide-react"; @@ -41,6 +41,17 @@ export default function BaseToast({ const startTime = useRef(0); const timerRef = useRef(null); + // ON MOBILE ONLY, when the toast is moved to any position but the top of the stack, the + // duration is reset to give each toast, when in the top position, its full duration + const isMobile = useCheckMobile(); + const [prevStackIndex, setPrevStackIndex] = useState(stackIndex); + const [resetCount, setResetCount] = useState(0); + if (isMobile && stackIndex !== prevStackIndex) { + setPrevStackIndex(stackIndex); + remainingTime.current = duration; + setResetCount((prev) => prev + 1); + } + useEffect(() => { if (isPersistent || !open) return; @@ -64,8 +75,6 @@ export default function BaseToast({ }; }, [isPaused, open, isPersistent, onOpenChange]); - const isMobile = useCheckMobile(); - return ( {!isPersistent && ( Date: Wed, 1 Jul 2026 12:35:19 -0400 Subject: [PATCH 11/15] Move duration pause to useEffect --- frontend/src/features/system-feedback/toast/base.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index bd4813268..098bb95d7 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -46,11 +46,13 @@ export default function BaseToast({ const isMobile = useCheckMobile(); const [prevStackIndex, setPrevStackIndex] = useState(stackIndex); const [resetCount, setResetCount] = useState(0); - if (isMobile && stackIndex !== prevStackIndex) { - setPrevStackIndex(stackIndex); - remainingTime.current = duration; - setResetCount((prev) => prev + 1); - } + useEffect(() => { + if (isMobile && stackIndex !== prevStackIndex) { + setPrevStackIndex(stackIndex); + remainingTime.current = duration; + setResetCount((prev) => prev + 1); + } + }, [isMobile, stackIndex, prevStackIndex, duration]); useEffect(() => { if (isPersistent || !open) return; From 5170e15ff271f77f86565e602b4565d8f022b0dd Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 1 Jul 2026 12:38:12 -0400 Subject: [PATCH 12/15] Clamp background color mix value --- frontend/src/features/system-feedback/toast/base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 098bb95d7..25bb47ff8 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -105,7 +105,7 @@ export default function BaseToast({ ? { zIndex: 2147483647 - stackIndex, scale: Math.max(0, 1 - stackIndex * 0.1), - backgroundColor: `color-mix(in oklab, var(--color-${backgroundColor}), var(--color-background) ${stackIndex * 40}%)`, + backgroundColor: `color-mix(in oklab, var(--color-${backgroundColor}), var(--color-background) ${Math.min(100, stackIndex * 40)}%)`, } : {}), }} From 02257de7691e2a48693ac2629f49d26d6f9efaf3 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 1 Jul 2026 12:41:05 -0400 Subject: [PATCH 13/15] Remove hidden toast content from focus tree --- frontend/src/features/system-feedback/toast/base.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/features/system-feedback/toast/base.tsx b/frontend/src/features/system-feedback/toast/base.tsx index 25bb47ff8..2f1effd22 100644 --- a/frontend/src/features/system-feedback/toast/base.tsx +++ b/frontend/src/features/system-feedback/toast/base.tsx @@ -127,6 +127,7 @@ export default function BaseToast({ "flex items-center justify-between gap-4", isMobile && stackIndex > 1 && "opacity-0", )} + aria-hidden={isMobile && stackIndex > 0} >
{icon}
@@ -159,6 +160,7 @@ export default function BaseToast({ !isPersistent && "opacity-0 focus-visible:opacity-100 group-hover:opacity-100", )} + tabIndex={isMobile && stackIndex > 0 ? -1 : undefined} > From f517e0b9722c10a2a8c74a0445d9c231cb133d1c Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 18 Jul 2026 16:33:36 -0400 Subject: [PATCH 14/15] Shorten toast removal delay --- frontend/src/features/system-feedback/toast/provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index ca24aaef4..846fcc311 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -100,7 +100,7 @@ export default function ToastProvider({ setTimeout(() => { setToasts((prevToasts) => prevToasts.filter((t) => t.id !== id)); - }, 400); + }, 100); }, []); return ( From 4931d7b70972b307ef52f56f5e14b162e1c5c408 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 18 Jul 2026 16:35:31 -0400 Subject: [PATCH 15/15] Lengthen desktop toast removal delay --- .../system-feedback/toast/provider.tsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/system-feedback/toast/provider.tsx b/frontend/src/features/system-feedback/toast/provider.tsx index 846fcc311..263a3ceaf 100644 --- a/frontend/src/features/system-feedback/toast/provider.tsx +++ b/frontend/src/features/system-feedback/toast/provider.tsx @@ -91,17 +91,23 @@ export default function ToastProvider({ [], ); - const removeToast = useCallback((id: number) => { - if (id === -1) return; - - setToasts((prevToasts) => - prevToasts.map((t) => (t.id === id ? { ...t, open: false } : t)), - ); - - setTimeout(() => { - setToasts((prevToasts) => prevToasts.filter((t) => t.id !== id)); - }, 100); - }, []); + const removeToast = useCallback( + (id: number) => { + if (id === -1) return; + + setToasts((prevToasts) => + prevToasts.map((t) => (t.id === id ? { ...t, open: false } : t)), + ); + + setTimeout( + () => { + setToasts((prevToasts) => prevToasts.filter((t) => t.id !== id)); + }, + isMobile ? 100 : 250, + ); + }, + [isMobile], + ); return (