fix: adjust toast notifications to prevent screen overflow on mobile
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
This commit is contained in:
@@ -49,21 +49,20 @@ pub fn SonnerTrigger(
|
|||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let variant_classes = match toast.variant {
|
let variant_classes = match toast.variant {
|
||||||
ToastType::Default => "bg-background text-foreground border-border",
|
ToastType::Default => "bg-background text-foreground border-border",
|
||||||
ToastType::Success => "bg-background text-foreground border-border [&_.icon]:text-success",
|
ToastType::Success => "bg-background text-foreground border-border [&_.icon]:text-green-500",
|
||||||
ToastType::Error => "bg-background text-foreground border-border [&_.icon]:text-destructive",
|
ToastType::Error => "bg-background text-foreground border-border [&_.icon]:text-destructive",
|
||||||
ToastType::Warning => "bg-background text-foreground border-border [&_.icon]:text-warning",
|
ToastType::Warning => "bg-background text-foreground border-border [&_.icon]:text-yellow-500",
|
||||||
ToastType::Info => "bg-background text-foreground border-border [&_.icon]:text-info",
|
ToastType::Info => "bg-background text-foreground border-border [&_.icon]:text-blue-500",
|
||||||
ToastType::Loading => "bg-background text-foreground border-border",
|
ToastType::Loading => "bg-background text-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sonner Stacking Logic
|
// Sonner Stacking Logic
|
||||||
// We calculate inverse index: 0 is the newest (top), 1 is older, etc.
|
|
||||||
let inverse_index = index;
|
let inverse_index = index;
|
||||||
let offset = inverse_index as f64 * 16.0;
|
let offset = inverse_index as f64 * 12.0;
|
||||||
let scale = 1.0 - (inverse_index as f64 * 0.05);
|
let scale = 1.0 - (inverse_index as f64 * 0.05);
|
||||||
let opacity = if inverse_index > 2 { 0.0 } else { 1.0 - (inverse_index as f64 * 0.2) };
|
let opacity = if inverse_index > 2 { 0.0 } else { 1.0 - (inverse_index as f64 * 0.15) };
|
||||||
|
|
||||||
let is_bottom = !position.to_string().contains("Top");
|
let is_bottom = position.to_string().contains("Bottom");
|
||||||
let y_direction = if is_bottom { -1.0 } else { 1.0 };
|
let y_direction = if is_bottom { -1.0 } else { 1.0 };
|
||||||
let translate_y = offset * y_direction;
|
let translate_y = offset * y_direction;
|
||||||
|
|
||||||
@@ -76,10 +75,10 @@ pub fn SonnerTrigger(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let icon = match toast.variant {
|
let icon = match toast.variant {
|
||||||
ToastType::Success => Some(view! { <span class="icon text-success">"✓"</span> }.into_any()),
|
ToastType::Success => Some(view! { <span class="icon font-bold">"✓"</span> }.into_any()),
|
||||||
ToastType::Error => Some(view! { <span class="icon text-destructive">"✕"</span> }.into_any()),
|
ToastType::Error => Some(view! { <span class="icon font-bold">"✕"</span> }.into_any()),
|
||||||
ToastType::Warning => Some(view! { <span class="icon text-warning">"⚠"</span> }.into_any()),
|
ToastType::Warning => Some(view! { <span class="icon font-bold">"⚠"</span> }.into_any()),
|
||||||
ToastType::Info => Some(view! { <span class="icon text-info">"ℹ"</span> }.into_any()),
|
ToastType::Info => Some(view! { <span class="icon font-bold">"ℹ"</span> }.into_any()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ pub fn SonnerTrigger(
|
|||||||
<div
|
<div
|
||||||
class=tw_merge!(
|
class=tw_merge!(
|
||||||
"absolute transition-all duration-300 ease-in-out cursor-pointer pointer-events-auto",
|
"absolute transition-all duration-300 ease-in-out cursor-pointer pointer-events-auto",
|
||||||
"flex items-center gap-3 min-w-[350px] p-4 rounded-lg border shadow-lg bg-card",
|
"flex items-center gap-3 w-full max-w-[calc(100vw-2rem)] sm:max-w-[380px] p-4 rounded-lg border shadow-lg bg-card",
|
||||||
if is_bottom { "bottom-0" } else { "top-0" },
|
if is_bottom { "bottom-0" } else { "top-0" },
|
||||||
variant_classes
|
variant_classes
|
||||||
)
|
)
|
||||||
@@ -99,15 +98,14 @@ pub fn SonnerTrigger(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-0.5 overflow-hidden">
|
||||||
<div class="text-sm font-semibold">{toast.title}</div>
|
<div class="text-sm font-semibold truncate leading-tight">{toast.title}</div>
|
||||||
{move || toast.description.as_ref().map(|d| view! { <div class="text-xs opacity-70">{d.clone()}</div> })}
|
{move || toast.description.as_ref().map(|d| view! { <div class="text-xs opacity-70 truncate">{d.clone()}</div> })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread local storage for global access
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static TOASTS: std::cell::RefCell<Option<RwSignal<Vec<ToastData>>>> = std::cell::RefCell::new(None);
|
static TOASTS: std::cell::RefCell<Option<RwSignal<Vec<ToastData>>>> = std::cell::RefCell::new(None);
|
||||||
}
|
}
|
||||||
@@ -124,26 +122,29 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
|||||||
let toasts = store.toasts;
|
let toasts = store.toasts;
|
||||||
let is_hovered = RwSignal::new(false);
|
let is_hovered = RwSignal::new(false);
|
||||||
|
|
||||||
let container_class = match position {
|
let (container_class, mobile_class) = match position {
|
||||||
SonnerPosition::TopLeft => "left-6 top-6 items-start",
|
SonnerPosition::TopLeft => ("left-6 top-6 items-start", "left-4 top-4"),
|
||||||
SonnerPosition::TopRight => "right-6 top-6 items-end",
|
SonnerPosition::TopRight => ("right-6 top-6 items-end", "right-4 top-4"),
|
||||||
SonnerPosition::TopCenter => "left-1/2 -translate-x-1/2 top-6 items-center",
|
SonnerPosition::TopCenter => ("left-1/2 -translate-x-1/2 top-6 items-center", "left-1/2 -translate-x-1/2 top-4"),
|
||||||
SonnerPosition::BottomCenter => "left-1/2 -translate-x-1/2 bottom-6 items-center",
|
SonnerPosition::BottomCenter => ("left-1/2 -translate-x-1/2 bottom-6 items-center", "left-1/2 -translate-x-1/2 bottom-4"),
|
||||||
SonnerPosition::BottomLeft => "left-6 bottom-6 items-start",
|
SonnerPosition::BottomLeft => ("left-6 bottom-6 items-start", "left-4 bottom-4"),
|
||||||
SonnerPosition::BottomRight => "right-6 bottom-6 items-end",
|
SonnerPosition::BottomRight => ("right-6 bottom-6 items-end", "right-4 bottom-4"),
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
class=tw_merge!("fixed z-[100] flex flex-col pointer-events-none min-h-[200px] w-[400px]", container_class)
|
class=tw_merge!(
|
||||||
|
"fixed z-[100] flex flex-col pointer-events-none min-h-[100px] w-full sm:w-[400px]",
|
||||||
|
container_class,
|
||||||
|
// Safe areas for mobile
|
||||||
|
"pb-[env(safe-area-inset-bottom)] pt-[env(safe-area-inset-top)] px-4 sm:px-0"
|
||||||
|
)
|
||||||
on:mouseenter=move |_| is_hovered.set(true)
|
on:mouseenter=move |_| is_hovered.set(true)
|
||||||
on:mouseleave=move |_| is_hovered.set(false)
|
on:mouseleave=move |_| is_hovered.set(false)
|
||||||
>
|
>
|
||||||
<For
|
<For
|
||||||
each=move || {
|
each=move || {
|
||||||
let list = toasts.get();
|
let list = toasts.get();
|
||||||
// Reverse the list so newest is at the end (for stacking)
|
|
||||||
// or newest is at the beginning (for display logic)
|
|
||||||
list.into_iter().rev().enumerate().collect::<Vec<_>>()
|
list.into_iter().rev().enumerate().collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
key=|(_, toast)| toast.id
|
key=|(_, toast)| toast.id
|
||||||
@@ -151,11 +152,10 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
|||||||
let id = toast.id;
|
let id = toast.id;
|
||||||
let total = toasts.with(|t| t.len());
|
let total = toasts.with(|t| t.len());
|
||||||
|
|
||||||
// If hovered, expand the stack
|
|
||||||
let expanded_style = move || {
|
let expanded_style = move || {
|
||||||
if is_hovered.get() {
|
if is_hovered.get() {
|
||||||
let offset = index as f64 * 70.0;
|
let offset = index as f64 * 64.0;
|
||||||
let is_bottom = !position.to_string().contains("Top");
|
let is_bottom = position.to_string().contains("Bottom");
|
||||||
let y_dir = if is_bottom { -1.0 } else { 1.0 };
|
let y_dir = if is_bottom { -1.0 } else { 1.0 };
|
||||||
format!("transform: translateY({}px) scale(1); opacity: 1;", offset * y_dir)
|
format!("transform: translateY({}px) scale(1); opacity: 1;", offset * y_dir)
|
||||||
} else {
|
} else {
|
||||||
@@ -164,7 +164,7 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div style=expanded_style>
|
<div class="contents" style=expanded_style>
|
||||||
<SonnerTrigger
|
<SonnerTrigger
|
||||||
toast=toast
|
toast=toast
|
||||||
index=index
|
index=index
|
||||||
@@ -182,7 +182,6 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
|||||||
}.into_any()
|
}.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global Helper Functions
|
|
||||||
pub fn toast(title: impl Into<String>, variant: ToastType) {
|
pub fn toast(title: impl Into<String>, variant: ToastType) {
|
||||||
let signal_opt = TOASTS.with(|t| *t.borrow());
|
let signal_opt = TOASTS.with(|t| *t.borrow());
|
||||||
|
|
||||||
@@ -218,4 +217,4 @@ pub fn toast_error(title: impl Into<String>) { toast(title, ToastType::Error); }
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn toast_warning(title: impl Into<String>) { toast(title, ToastType::Warning); }
|
pub fn toast_warning(title: impl Into<String>) { toast(title, ToastType::Warning); }
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn toast_info(title: impl Into<String>) { toast(title, ToastType::Info); }
|
pub fn toast_info(title: impl Into<String>) { toast(title, ToastType::Info); }
|
||||||
Reference in New Issue
Block a user