Compare commits
5 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c0c0a0919 | ||
|
|
3158a11229 | ||
|
|
45f5d1b678 | ||
|
|
c8e3caa4fc | ||
|
|
98555f16ca |
@@ -1,11 +1,13 @@
|
||||
use crate::components::layout::protected::Protected;
|
||||
use crate::components::ui::skeleton::Skeleton;
|
||||
use crate::components::ui::card::{Card, CardHeader, CardContent};
|
||||
use crate::components::torrent::table::TorrentTable;
|
||||
use crate::components::auth::login::Login;
|
||||
use crate::components::auth::setup::Setup;
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use leptos_router::components::{Router, Routes, Route};
|
||||
use leptos_router::hooks::use_navigate;
|
||||
use leptos_router::hooks::{use_navigate, use_location};
|
||||
use crate::components::ui::toast::Toaster;
|
||||
use crate::components::hooks::use_theme_mode::ThemeMode;
|
||||
|
||||
@@ -30,7 +32,9 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<Toaster />
|
||||
<InnerApp />
|
||||
<Router>
|
||||
<InnerApp />
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
fn InnerApp() -> impl IntoView {
|
||||
crate::store::provide_torrent_store();
|
||||
let store = use_context::<crate::store::TorrentStore>();
|
||||
let loc = use_location();
|
||||
|
||||
let is_loading = signal(true);
|
||||
let is_authenticated = signal(false);
|
||||
@@ -97,119 +102,148 @@ fn InnerApp() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div class="relative w-full h-screen" style="height: 100dvh;">
|
||||
<Router>
|
||||
<Routes fallback=|| view! { <div class="p-4">"404 Not Found"</div> }>
|
||||
<Route path=leptos_router::path!("/login") view=move || {
|
||||
let authenticated = is_authenticated.0.get();
|
||||
let setup_needed = needs_setup.0.get();
|
||||
|
||||
Effect::new(move |_| {
|
||||
if setup_needed {
|
||||
let navigate = use_navigate();
|
||||
<Routes fallback=|| view! { <div class="p-4">"404 Not Found"</div> }>
|
||||
<Route path=leptos_router::path!("/login") view=move || {
|
||||
let authenticated = is_authenticated.0.get();
|
||||
let setup_needed = needs_setup.0.get();
|
||||
|
||||
Effect::new(move |_| {
|
||||
if setup_needed {
|
||||
let navigate = use_navigate();
|
||||
navigate("/setup", Default::default());
|
||||
} else if authenticated {
|
||||
log::info!("Already authenticated, redirecting to home");
|
||||
let navigate = use_navigate();
|
||||
navigate("/", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! { <Login /> }
|
||||
} />
|
||||
<Route path=leptos_router::path!("/setup") view=move || {
|
||||
Effect::new(move |_| {
|
||||
if is_authenticated.0.get() {
|
||||
let navigate = use_navigate();
|
||||
navigate("/", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! { <Setup /> }
|
||||
} />
|
||||
|
||||
<Route path=leptos_router::path!("/") view=move || {
|
||||
let navigate = use_navigate();
|
||||
Effect::new(move |_| {
|
||||
if !is_loading.0.get() {
|
||||
if needs_setup.0.get() {
|
||||
log::info!("Setup not completed, redirecting to setup");
|
||||
navigate("/setup", Default::default());
|
||||
} else if authenticated {
|
||||
log::info!("Already authenticated, redirecting to home");
|
||||
let navigate = use_navigate();
|
||||
navigate("/", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! { <Login /> }
|
||||
} />
|
||||
<Route path=leptos_router::path!("/setup") view=move || {
|
||||
Effect::new(move |_| {
|
||||
if is_authenticated.0.get() {
|
||||
let navigate = use_navigate();
|
||||
navigate("/", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! { <Setup /> }
|
||||
} />
|
||||
|
||||
<Route path=leptos_router::path!("/") view=move || {
|
||||
let navigate = use_navigate();
|
||||
Effect::new(move |_| {
|
||||
if !is_loading.0.get() {
|
||||
if needs_setup.0.get() {
|
||||
log::info!("Setup not completed, redirecting to setup");
|
||||
navigate("/setup", Default::default());
|
||||
} else if !is_authenticated.0.get() {
|
||||
log::info!("Not authenticated, redirecting to login");
|
||||
navigate("/login", Default::default());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Show when=move || !is_loading.0.get() fallback=|| view! {
|
||||
<div class="flex h-screen bg-background">
|
||||
// Sidebar skeleton
|
||||
<div class="w-56 border-r border-border p-4 space-y-4">
|
||||
<div class="h-8 w-3/4 animate-pulse rounded-md bg-muted" />
|
||||
<div class="space-y-2">
|
||||
<div class="h-6 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-6 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-6 w-4/5 animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-6 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-6 w-3/5 animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-6 w-full animate-pulse rounded-md bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
// Main content skeleton
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div class="border-b border-border p-4 flex items-center gap-4">
|
||||
<div class="h-8 w-48 animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-8 w-64 animate-pulse rounded-md bg-muted" />
|
||||
<div class="ml-auto"><div class="h-8 w-24 animate-pulse rounded-md bg-muted" /></div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 space-y-3">
|
||||
<div class="h-10 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-10 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-10 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-10 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-10 w-full animate-pulse rounded-md bg-muted" />
|
||||
<div class="h-10 w-3/4 animate-pulse rounded-md bg-muted" />
|
||||
</div>
|
||||
<div class="border-t border-border p-3">
|
||||
<div class="h-5 w-96 animate-pulse rounded-md bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()>
|
||||
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
||||
<Protected>
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<TorrentTable />
|
||||
</div>
|
||||
</div>
|
||||
</Protected>
|
||||
</Show>
|
||||
</Show>
|
||||
}.into_any()
|
||||
}/>
|
||||
|
||||
<Route path=leptos_router::path!("/settings") view=move || {
|
||||
Effect::new(move |_| {
|
||||
if !is_authenticated.0.get() {
|
||||
let navigate = use_navigate();
|
||||
} else if !is_authenticated.0.get() {
|
||||
log::info!("Not authenticated, redirecting to login");
|
||||
navigate("/login", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Show when=move || !is_loading.0.get() fallback=|| ()>
|
||||
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
||||
<Protected>
|
||||
<div class="p-4">"Settings Page (Coming Soon)"</div>
|
||||
</Protected>
|
||||
</Show>
|
||||
</Show>
|
||||
}
|
||||
}/>
|
||||
</Routes>
|
||||
</Router>
|
||||
});
|
||||
|
||||
view! {
|
||||
<Show when=move || !is_loading.0.get() fallback=move || {
|
||||
let path = loc.pathname.get();
|
||||
if path == "/login" {
|
||||
// Login Skeleton
|
||||
view! {
|
||||
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||
<Card class="w-full max-w-sm shadow-lg border-none">
|
||||
<CardHeader class="pb-2 items-center space-y-4">
|
||||
<Skeleton class="w-12 h-12 rounded-xl" />
|
||||
<Skeleton class="h-8 w-32" />
|
||||
<Skeleton class="h-4 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent class="pt-4 space-y-6">
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-4 w-24" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-4 w-24" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
</div>
|
||||
<Skeleton class="h-10 w-full rounded-md mt-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
// Dashboard Skeleton
|
||||
view! {
|
||||
<div class="flex h-screen bg-background">
|
||||
// Sidebar skeleton
|
||||
<div class="w-56 border-r border-border p-4 space-y-4">
|
||||
<Skeleton class="h-8 w-3/4" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="h-6 w-full" />
|
||||
<Skeleton class="h-6 w-full" />
|
||||
<Skeleton class="h-6 w-4/5" />
|
||||
<Skeleton class="h-6 w-full" />
|
||||
<Skeleton class="h-6 w-3/5" />
|
||||
<Skeleton class="h-6 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
// Main content skeleton
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div class="border-b border-border p-4 flex items-center gap-4">
|
||||
<Skeleton class="h-8 w-48" />
|
||||
<Skeleton class="h-8 w-64" />
|
||||
<div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
|
||||
</div>
|
||||
<div class="flex-1 p-4 space-y-3">
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-3/4" />
|
||||
</div>
|
||||
<div class="border-t border-border p-3">
|
||||
<Skeleton class="h-5 w-96" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
}
|
||||
}>
|
||||
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
||||
<Protected>
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<TorrentTable />
|
||||
</div>
|
||||
</div>
|
||||
</Protected>
|
||||
</Show>
|
||||
</Show>
|
||||
}.into_any()
|
||||
}/>
|
||||
|
||||
<Route path=leptos_router::path!("/settings") view=move || {
|
||||
let authenticated = is_authenticated.0.get();
|
||||
Effect::new(move |_| {
|
||||
if !authenticated {
|
||||
let navigate = use_navigate();
|
||||
navigate("/login", Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Show when=move || !is_loading.0.get() fallback=|| ()>
|
||||
<Show when=move || authenticated fallback=|| ()>
|
||||
<Protected>
|
||||
<div class="p-4">"Settings Page (Coming Soon)"</div>
|
||||
</Protected>
|
||||
</Show>
|
||||
</Show>
|
||||
}
|
||||
}/>
|
||||
</Routes>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,9 +287,10 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// --- MAIN TABLE ---
|
||||
// --- MAIN CONTENT ---
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<DataTableWrapper class="h-full bg-card/50">
|
||||
// Desktop Table View
|
||||
<DataTableWrapper class="hidden md:block h-full bg-card/50">
|
||||
<div class="h-full overflow-auto">
|
||||
<DataTable>
|
||||
<DataTableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
|
||||
@@ -305,49 +306,49 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
</DataTableHead>
|
||||
|
||||
{move || visible_columns.get().contains("Name").then(|| view! {
|
||||
<DataTableHead class="cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<DataTableHead class="cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground" on:click=move |_| handle_sort(SortColumn::Name)>
|
||||
<div class="flex items-center gap-2">"Name" {move || sort_icon(SortColumn::Name)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Size").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground" on:click=move |_| handle_sort(SortColumn::Size)>
|
||||
<div class="flex items-center gap-2">"Size" {move || sort_icon(SortColumn::Size)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Progress").then(|| view! {
|
||||
<DataTableHead class="w-48 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<DataTableHead class="w-48 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground" on:click=move |_| handle_sort(SortColumn::Progress)>
|
||||
<div class="flex items-center gap-2">"Progress" {move || sort_icon(SortColumn::Progress)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("Status").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||
<div class="flex items-center gap-2">"Status" {move || sort_icon(SortColumn::Status)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("DownSpeed").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground text-right" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"DL Speed" {move || sort_icon(SortColumn::DownSpeed)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("UpSpeed").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground text-right" on:click=move |_| handle_sort(SortColumn::UpSpeed)>
|
||||
<div class="flex items-center justify-end gap-2">"UP Speed" {move || sort_icon(SortColumn::UpSpeed)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("ETA").then(|| view! {
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<DataTableHead class="w-24 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground text-right" on:click=move |_| handle_sort(SortColumn::ETA)>
|
||||
<div class="flex items-center justify-end gap-2">"ETA" {move || sort_icon(SortColumn::ETA)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
|
||||
{move || visible_columns.get().contains("AddedDate").then(|| view! {
|
||||
<DataTableHead class="w-32 cursor-pointer group select-none text-right" on:click=move |_| handle_sort(SortColumn::AddedDate)>
|
||||
<DataTableHead class="w-32 cursor-pointer group select-none transition-all duration-100 active:scale-[0.98] hover:bg-muted/30 hover:text-foreground text-right" on:click=move |_| handle_sort(SortColumn::AddedDate)>
|
||||
<div class="flex items-center justify-end gap-2">"Date" {move || sort_icon(SortColumn::AddedDate)}</div>
|
||||
</DataTableHead>
|
||||
}).into_any()}
|
||||
@@ -407,6 +408,26 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
</DataTable>
|
||||
</div>
|
||||
</DataTableWrapper>
|
||||
|
||||
// Mobile Card View
|
||||
<div class="block md:hidden h-full overflow-y-auto space-y-3 pb-20">
|
||||
<Show
|
||||
when=move || !filtered_hashes.get().is_empty()
|
||||
fallback=move || view! {
|
||||
<div class="flex flex-col items-center justify-center h-64 opacity-50 text-muted-foreground">
|
||||
<Inbox class="size-12 mb-2" />
|
||||
<p>"Torrent Bulunamadı"</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
>
|
||||
<For each=move || filtered_hashes.get() key=|hash| hash.clone() children={
|
||||
let on_action = on_action.clone();
|
||||
move |hash| {
|
||||
view! { <TorrentCard hash=hash.clone() on_action=on_action.clone() /> }
|
||||
}
|
||||
} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center justify-between px-2 py-1 text-[11px] text-muted-foreground bg-muted/20 border rounded-md">
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod select;
|
||||
pub mod separator;
|
||||
pub mod sheet;
|
||||
pub mod sidenav;
|
||||
pub mod skeleton;
|
||||
pub mod svg_icon;
|
||||
pub mod table;
|
||||
pub mod theme_toggle;
|
||||
|
||||
13
frontend/src/components/ui/skeleton.rs
Normal file
13
frontend/src/components/ui/skeleton.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
use tw_merge::tw_merge;
|
||||
|
||||
#[component]
|
||||
pub fn Skeleton(
|
||||
#[prop(optional, into)] class: String,
|
||||
) -> impl IntoView {
|
||||
let class = tw_merge!(
|
||||
"animate-pulse rounded-md bg-muted",
|
||||
class
|
||||
);
|
||||
view! { <div class=class /> }
|
||||
}
|
||||
@@ -66,18 +66,11 @@ pub fn SonnerTrigger(
|
||||
_ => "bg-primary",
|
||||
};
|
||||
|
||||
// List Layout Logic (No stacking/overlapping)
|
||||
// Simplified Style (No manual translateY needed with Flexbox)
|
||||
let style = move || {
|
||||
let is_bottom = position.to_string().contains("Bottom");
|
||||
let y_direction = if is_bottom { -1.0 } else { 1.0 };
|
||||
|
||||
// Dynamic Y position based on index (index 0 is the newest/bottom-most in the view)
|
||||
let y = index as f64 * 72.0; // Height (approx 64px) + Gap (8px)
|
||||
|
||||
format!(
|
||||
"z-index: {}; transform: translateY({}px); opacity: 1; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;",
|
||||
total - index,
|
||||
y * y_direction
|
||||
"z-index: {}; opacity: 1; transition: all 0.3s ease;",
|
||||
total - index
|
||||
)
|
||||
};
|
||||
|
||||
@@ -105,9 +98,8 @@ pub fn SonnerTrigger(
|
||||
view! {
|
||||
<div
|
||||
class=move || tw_merge!(
|
||||
"absolute transition-all duration-300 ease-in-out cursor-pointer pointer-events-auto overflow-hidden",
|
||||
"relative transition-all duration-300 ease-in-out cursor-pointer pointer-events-auto",
|
||||
"flex items-center gap-3 w-full max-w-[calc(100vw-2rem)] sm:max-w-[380px] p-4 rounded-lg border shadow-xl bg-card",
|
||||
if position.to_string().contains("Bottom") { "bottom-0" } else { "top-0" },
|
||||
variant_classes,
|
||||
animation_class()
|
||||
)
|
||||
@@ -150,7 +142,8 @@ pub fn provide_toaster() {
|
||||
pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosition) -> impl IntoView {
|
||||
let store = use_context::<ToasterStore>().expect("Toaster context not found");
|
||||
let toasts = store.toasts;
|
||||
let is_hovered = RwSignal::new(false);
|
||||
|
||||
let is_bottom = position.to_string().contains("Bottom");
|
||||
|
||||
let container_class = match position {
|
||||
SonnerPosition::TopLeft => "left-6 top-6 items-start",
|
||||
@@ -175,17 +168,16 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
||||
</style>
|
||||
<div
|
||||
class=tw_merge!(
|
||||
"fixed z-[100] flex flex-col pointer-events-none min-h-[100px] w-full sm:w-[400px]",
|
||||
"fixed z-[100] flex gap-3 pointer-events-none w-full sm:w-[400px]",
|
||||
if is_bottom { "flex-col-reverse" } else { "flex-col" },
|
||||
container_class,
|
||||
"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:mouseleave=move |_| is_hovered.set(false)
|
||||
>
|
||||
<For
|
||||
each=move || {
|
||||
let list = toasts.get();
|
||||
list.into_iter().rev().enumerate().collect::<Vec<_>>()
|
||||
list.into_iter().enumerate().collect::<Vec<_>>()
|
||||
}
|
||||
key=|(_, toast)| toast.id
|
||||
children=move |(index, toast)| {
|
||||
@@ -199,7 +191,7 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
|
||||
index=index
|
||||
total=total
|
||||
position=position
|
||||
is_expanded=is_hovered.into()
|
||||
is_expanded=Signal::derive(move || true)
|
||||
on_dismiss=Callback::new(move |_| {
|
||||
is_exiting.set(true);
|
||||
leptos::task::spawn_local(async move {
|
||||
|
||||
Reference in New Issue
Block a user