Compare commits

...

5 Commits

Author SHA1 Message Date
spinline
945f4718eb feat: match bulk action button style with columns button and add progress bar to toasts
All checks were successful
Build MIPS Binary / build (push) Successful in 5m33s
2026-02-12 19:42:32 +03:00
spinline
6a2952c6f3 fix: resolve 404 error for lock_scroll.js by including it in Trunk build and loading globally
All checks were successful
Build MIPS Binary / build (push) Successful in 5m32s
2026-02-12 01:51:38 +03:00
spinline
03b63dd5d0 chore: remove old search box from toolbar and modernize add button
All checks were successful
Build MIPS Binary / build (push) Successful in 5m29s
2026-02-12 01:40:17 +03:00
spinline
7717dffc56 fix: adjust toast notifications to prevent screen overflow on mobile
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
2026-02-12 01:38:29 +03:00
spinline
3a2cab7ca7 fix: resolve nested button styles in bulk actions and clean up UI modules
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
2026-02-12 01:37:00 +03:00
9 changed files with 71 additions and 76 deletions

View File

@@ -25,6 +25,8 @@
<link data-trunk rel="copy-file" href="manifest.json" /> <link data-trunk rel="copy-file" href="manifest.json" />
<link data-trunk rel="copy-file" href="icon-192.png" /> <link data-trunk rel="copy-file" href="icon-192.png" />
<link data-trunk rel="copy-file" href="icon-512.png" /> <link data-trunk rel="copy-file" href="icon-512.png" />
<link data-trunk rel="copy-file" href="public/lock_scroll.js" />
<script src="/lock_scroll.js"></script>
<link data-trunk rel="copy-file" href="sw.js" /> <link data-trunk rel="copy-file" href="sw.js" />
<script> <script>
(function () { (function () {

View File

@@ -1,20 +1,12 @@
use leptos::prelude::*; use leptos::prelude::*;
use crate::components::torrent::add_torrent::AddTorrentDialog; use crate::components::torrent::add_torrent::AddTorrentDialog;
use crate::components::ui::button::{Button};
#[component] #[component]
pub fn Toolbar() -> impl IntoView { pub fn Toolbar() -> impl IntoView {
let show_add_modal = signal(false); let show_add_modal = signal(false);
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
let is_mobile_menu_open = use_context::<RwSignal<bool>>().expect("mobile menu state not provided"); let is_mobile_menu_open = use_context::<RwSignal<bool>>().expect("mobile menu state not provided");
let search_value = RwSignal::new(String::new());
// Sync search_value to store
Effect::new(move |_| {
let val = search_value.get();
store.search_query.set(val);
});
view! { view! {
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);"> <div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
// Sol kısım: Menü butonu + Add Torrent // Sol kısım: Menü butonu + Add Torrent
@@ -27,33 +19,20 @@ pub fn Toolbar() -> impl IntoView {
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button> </button>
<button <Button
class="inline-flex items-center justify-center gap-2 h-9 px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 transition-all active:scale-[0.98]"
on:click=move |_| show_add_modal.1.set(true) on:click=move |_| show_add_modal.1.set(true)
class="gap-2"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
<span class="hidden sm:inline">"Add Torrent"</span> <span class="hidden sm:inline">"Add Torrent"</span>
<span class="sm:hidden">"Add"</span> <span class="sm:hidden">"Add"</span>
</button> </Button>
</div> </div>
// Sağ kısım: Search kutusu // Sağ kısım boşaltıldı (arama kutusu kaldırıldı)
<div class="flex flex-1 items-center justify-end gap-2"> <div class="flex flex-1 items-center justify-end gap-2">
<div class="hidden md:flex items-center gap-2 w-full max-w-xs">
<div class="relative flex-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
<input
type="search"
placeholder="Search..."
class="file:text-foreground placeholder:text-muted-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 md:text-sm pl-8"
bind:value=search_value
/>
</div>
</div>
</div> </div>
<Show when=move || show_add_modal.0.get()> <Show when=move || show_add_modal.0.get()>

View File

@@ -220,11 +220,9 @@ pub fn TorrentTable() -> impl IntoView {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Show when=move || has_selection.get()> <Show when=move || has_selection.get()>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger class="gap-2 bg-secondary text-secondary-foreground border-none hover:bg-secondary/80">
<Button variant=ButtonVariant::Secondary size=ButtonSize::Sm class="gap-2"> <Ellipsis class="size-4" />
<Ellipsis class="size-4" /> {move || format!("Toplu İşlem ({})", selected_count.get())}
{move || format!("Toplu İşlem ({})", selected_count.get())}
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-48"> <DropdownMenuContent class="w-48">
<DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel> <DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel>
@@ -239,10 +237,10 @@ pub fn TorrentTable() -> impl IntoView {
<div class="my-1 h-px bg-border" /> <div class="my-1 h-px bg-border" />
<AlertDialog> <AlertDialog>
<AlertDialogTrigger class="w-full"> <AlertDialogTrigger class="w-full text-left">
<DropdownMenuItem class="text-destructive focus:bg-destructive/10"> <div class="inline-flex gap-2 items-center w-full rounded-sm px-2 py-1.5 text-sm transition-colors text-destructive hover:bg-destructive/10 focus:bg-destructive/10">
<Trash2 class="mr-2 size-4" /> "Toplu Sil" <Trash2 class="size-4" /> "Toplu Sil"
</DropdownMenuItem> </div>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View File

@@ -237,8 +237,6 @@ pub fn ContextMenuContent(
let target_id_for_script = ctx.target_id.clone(); let target_id_for_script = ctx.target_id.clone();
view! { view! {
<script src="/lock_scroll.js"></script>
<div <div
data-name="ContextMenuContent" data-name="ContextMenuContent"
class=class class=class

View File

@@ -280,8 +280,6 @@ pub fn DropdownMenuContent(
}; };
view! { view! {
<script src="/hooks/lock_scroll.js"></script>
<div <div
data-name="DropdownMenuContent" data-name="DropdownMenuContent"
class=class class=class

View File

@@ -180,8 +180,6 @@ pub fn MultiSelectContent(children: Children, #[prop(optional, into)] class: Str
let (on_scroll, can_scroll_up_signal, can_scroll_down_signal) = use_can_scroll_vertical(); let (on_scroll, can_scroll_up_signal, can_scroll_down_signal) = use_can_scroll_vertical();
view! { view! {
<script src="/lock_scroll.js"></script>
<div <div
data-name="MultiSelectContent" data-name="MultiSelectContent"
class=class class=class

View File

@@ -172,8 +172,6 @@ pub fn SelectContent(
let (on_scroll, can_scroll_up_signal, can_scroll_down_signal) = use_can_scroll_vertical(); let (on_scroll, can_scroll_up_signal, can_scroll_down_signal) = use_can_scroll_vertical();
view! { view! {
<script src="/lock_scroll.js"></script>
<div <div
data-name="SelectContent" data-name="SelectContent"
class=merged_class class=merged_class

View File

@@ -49,21 +49,28 @@ 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 let bar_color = match toast.variant {
// We calculate inverse index: 0 is the newest (top), 1 is older, etc. ToastType::Success => "bg-green-500",
let inverse_index = index; ToastType::Error => "bg-destructive",
let offset = inverse_index as f64 * 16.0; ToastType::Warning => "bg-yellow-500",
let scale = 1.0 - (inverse_index as f64 * 0.05); ToastType::Info => "bg-blue-500",
let opacity = if inverse_index > 2 { 0.0 } else { 1.0 - (inverse_index as f64 * 0.2) }; _ => "bg-primary",
};
let is_bottom = !position.to_string().contains("Top"); // Sonner Stacking Logic
let inverse_index = index;
let offset = inverse_index as f64 * 12.0;
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.15) };
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,18 +83,30 @@ 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,
}; };
view! { view! {
<style>
"
@keyframes sonner-progress {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
.sonner-progress-bar {
animation: sonner-progress linear forwards;
transform-origin: left;
}
"
</style>
<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 overflow-hidden",
"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 +118,20 @@ pub fn SonnerTrigger(
} }
> >
{icon} {icon}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-0.5 overflow-hidden flex-1">
<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>
// Progress Bar
<div
class=tw_merge!("absolute bottom-0 left-0 h-1 w-full sonner-progress-bar opacity-40", bar_color)
style=format!("animation-duration: {}ms;", toast.duration)
/>
</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);
} }
@@ -126,24 +150,26 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
let container_class = match position { let container_class = match position {
SonnerPosition::TopLeft => "left-6 top-6 items-start", SonnerPosition::TopLeft => "left-6 top-6 items-start",
SonnerPosition::TopRight => "right-6 top-6 items-end", SonnerPosition::TopRight => ("right-6 top-6 items-end"),
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"),
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"),
SonnerPosition::BottomLeft => "left-6 bottom-6 items-start", SonnerPosition::BottomLeft => ("left-6 bottom-6 items-start"),
SonnerPosition::BottomRight => "right-6 bottom-6 items-end", SonnerPosition::BottomRight => ("right-6 bottom-6 items-end"),
}; };
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,
"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 +177,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 +189,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 +207,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());