Compare commits

...

10 Commits

Author SHA1 Message Date
spinline
c8139f9338 chore: comprehensive cleanup of unused imports, dead code and compiler warnings
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-12 23:43:33 +03:00
spinline
a3735d0931 fix: resolve compilation errors related to JsCast and AlertDialogTrigger attributes
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-12 23:39:17 +03:00
spinline
55f00729ee fix: relocate AlertDialog outside of DropdownMenu to ensure proper centering
Some checks failed
Build MIPS Binary / build (push) Failing after 30s
2026-02-12 23:37:36 +03:00
spinline
275f4a91b2 fix: change href to src in Trunk script tag to resolve build error
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-12 23:34:06 +03:00
spinline
025a0c4a57 fix: use script tag for Trunk rust asset to resolve preload warnings
Some checks failed
Build MIPS Binary / build (push) Failing after 25s
2026-02-12 23:32:37 +03:00
spinline
b29f9f3cc2 fix: align AlertDialog structure with project standards using AlertDialogBody
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-12 23:30:42 +03:00
spinline
feede5c5b4 fix: resolve compilation type error and cleanup unused imports in app.rs
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-12 23:26:45 +03:00
spinline
c1306a32a9 fix: use data-preload='false' and revert SW strategy to resolve browser warnings
Some checks failed
Build MIPS Binary / build (push) Failing after 44s
2026-02-12 23:23:40 +03:00
spinline
ed5fba4b46 fix: further refine alert dialog styling and button layout
Some checks failed
Build MIPS Binary / build (push) Failing after 34s
2026-02-12 23:22:07 +03:00
spinline
f149603ac8 fix: improve bulk delete dialog styling and responsive layout
All checks were successful
Build MIPS Binary / build (push) Successful in 1m54s
2026-02-12 23:17:40 +03:00
12 changed files with 150 additions and 221 deletions

View File

@@ -20,7 +20,7 @@
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<!-- Trunk Assets --> <!-- Trunk Assets -->
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" data-no-preload /> <script data-trunk rel="rust" src="Cargo.toml" data-wasm-opt="0" data-preload="false"></script>
<link data-trunk rel="css" href="public/tailwind.css" /> <link data-trunk rel="css" href="public/tailwind.css" />
<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" />

View File

@@ -1,13 +1,12 @@
use crate::components::layout::protected::Protected; use crate::components::layout::protected::Protected;
use crate::components::ui::skeleton::Skeleton; use crate::components::ui::skeleton::Skeleton;
use crate::components::ui::card::{Card, CardHeader, CardContent};
use crate::components::torrent::table::TorrentTable; use crate::components::torrent::table::TorrentTable;
use crate::components::auth::login::Login; use crate::components::auth::login::Login;
use crate::components::auth::setup::Setup; use crate::components::auth::setup::Setup;
use leptos::prelude::*; use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use leptos_router::components::{Router, Routes, Route}; use leptos_router::components::{Router, Routes, Route};
use leptos_router::hooks::{use_navigate, use_location}; use leptos_router::hooks::use_navigate;
use crate::components::ui::toast::Toaster; use crate::components::ui::toast::Toaster;
use crate::components::hooks::use_theme_mode::ThemeMode; use crate::components::hooks::use_theme_mode::ThemeMode;
@@ -42,7 +41,6 @@ pub fn App() -> impl IntoView {
fn InnerApp() -> impl IntoView { fn InnerApp() -> impl IntoView {
crate::store::provide_torrent_store(); crate::store::provide_torrent_store();
let store = use_context::<crate::store::TorrentStore>(); let store = use_context::<crate::store::TorrentStore>();
let loc = use_location();
let is_loading = signal(true); let is_loading = signal(true);
let is_authenticated = signal(false); let is_authenticated = signal(false);
@@ -131,71 +129,71 @@ fn InnerApp() -> impl IntoView {
view! { <Setup /> } view! { <Setup /> }
} /> } />
<Route path=leptos_router::path!("/") view=move || { <Route path=leptos_router::path!("/") view=move || {
let navigate = use_navigate(); let navigate = use_navigate();
Effect::new(move |_| { Effect::new(move |_| {
if !is_loading.0.get() { if !is_loading.0.get() {
if needs_setup.0.get() { if needs_setup.0.get() {
log::info!("Setup not completed, redirecting to setup"); log::info!("Setup not completed, redirecting to setup");
navigate("/setup", Default::default()); navigate("/setup", Default::default());
} else if !is_authenticated.0.get() { } else if !is_authenticated.0.get() {
log::info!("Not authenticated, redirecting to login"); log::info!("Not authenticated, redirecting to login");
navigate("/login", Default::default()); navigate("/login", Default::default());
}
} }
}); }
});
view! {
<Show when=move || !is_loading.0.get() fallback=|| { view! {
// Standard 1: Always show Dashboard Skeleton <Show when=move || !is_loading.0.get() fallback=|| {
view! { // Standard 1: Always show Dashboard Skeleton
<div class="flex h-screen bg-background text-foreground overflow-hidden"> view! {
// Sidebar skeleton <div class="flex h-screen bg-background text-foreground overflow-hidden">
<div class="w-56 border-r border-border p-4 space-y-4"> // Sidebar skeleton
<Skeleton class="h-8 w-3/4" /> <div class="w-56 border-r border-border p-4 space-y-4">
<div class="space-y-2"> <Skeleton class="h-8 w-3/4" />
<Skeleton class="h-6 w-full" /> <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-full" /> <Skeleton class="h-6 w-4/5" />
<Skeleton class="h-6 w-3/5" /> <Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-full" /> <Skeleton class="h-6 w-3/5" />
</div> <Skeleton class="h-6 w-full" />
</div>
// Main content skeleton
<div class="flex-1 flex flex-col min-w-0">
<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>
</div> </div>
}.into_any() // Main content skeleton
}> <div class="flex-1 flex flex-col min-w-0">
<Show when=move || is_authenticated.0.get() fallback=|| ()> <div class="border-b border-border p-4 flex items-center gap-4">
<Protected> <Skeleton class="h-8 w-48" />
<div class="flex flex-col h-full overflow-hidden"> <Skeleton class="h-8 w-64" />
<div class="flex-1 overflow-hidden"> <div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
<TorrentTable />
</div>
</div> </div>
</Protected> <div class="flex-1 p-4 space-y-3">
</Show> <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() </Show>
}/> }.into_any()
}/>
<Route path=leptos_router::path!("/settings") view=move || { <Route path=leptos_router::path!("/settings") view=move || {
let authenticated = is_authenticated.0.get(); let authenticated = is_authenticated.0.get();

View File

@@ -1,5 +1,3 @@
use leptos::prelude::*;
pub fn use_random_id_for(prefix: &str) -> String { pub fn use_random_id_for(prefix: &str) -> String {
format!("{}_{}", prefix, js_sys::Math::random().to_string().replace(".", "")) format!("{}_{}", prefix, js_sys::Math::random().to_string().replace(".", ""))
} }

View File

@@ -8,26 +8,15 @@ pub struct ThemeMode {
const LOCALSTORAGE_KEY: &str = "darkmode"; const LOCALSTORAGE_KEY: &str = "darkmode";
/// Hook to access the dark mode context
///
/// Returns the ThemeMode instance from context for easy access
pub fn use_theme_mode() -> ThemeMode { pub fn use_theme_mode() -> ThemeMode {
expect_context::<ThemeMode>() expect_context::<ThemeMode>()
} }
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
impl ThemeMode { impl ThemeMode {
#[must_use]
/// Initializes a new ThemeMode instance.
pub fn init() -> Self { pub fn init() -> Self {
let theme_mode = Self { state: RwSignal::new(false) }; let theme_mode = Self { state: RwSignal::new(false) };
provide_context(theme_mode); provide_context(theme_mode);
// Use Effect to handle browser-only initialization
Effect::new(move |_| { Effect::new(move |_| {
let initial = Self::get_storage_state().unwrap_or(Self::prefers_dark_mode()); let initial = Self::get_storage_state().unwrap_or(Self::prefers_dark_mode());
theme_mode.state.set(initial); theme_mode.state.set(initial);
@@ -43,45 +32,14 @@ impl ThemeMode {
}); });
} }
pub fn set_dark(&self) {
self.set(true);
}
pub fn set_light(&self) {
self.set(false);
}
/// - `dark`: Set to `true` for dark mode, and `false` for light mode.
pub fn set(&self, dark: bool) {
self.state.set(dark);
Self::set_storage_state(dark);
}
#[must_use]
pub fn get(&self) -> bool { pub fn get(&self) -> bool {
self.state.get() self.state.get()
} }
#[must_use]
pub fn is_dark(&self) -> bool {
self.state.get()
}
#[must_use]
pub fn is_light(&self) -> bool {
!self.state.get()
}
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
/// Retrieves the local storage object, if available.
fn get_storage() -> Option<Storage> { fn get_storage() -> Option<Storage> {
window().local_storage().ok().flatten() window().local_storage().ok().flatten()
} }
/// Retrieves the dark mode state from local storage, if available.
fn get_storage_state() -> Option<bool> { fn get_storage_state() -> Option<bool> {
Self::get_storage() Self::get_storage()
.and_then(|storage| storage.get(LOCALSTORAGE_KEY).ok()) .and_then(|storage| storage.get(LOCALSTORAGE_KEY).ok())
@@ -89,7 +47,6 @@ impl ThemeMode {
.and_then(|entry| entry.parse::<bool>().ok()) .and_then(|entry| entry.parse::<bool>().ok())
} }
/// Checks whether the user's system prefers dark mode based on media queries.
fn prefers_dark_mode() -> bool { fn prefers_dark_mode() -> bool {
window() window()
.match_media("(prefers-color-scheme: dark)") .match_media("(prefers-color-scheme: dark)")
@@ -99,10 +56,9 @@ impl ThemeMode {
.unwrap_or_default() .unwrap_or_default()
} }
/// Stores the dark mode state in local storage.
fn set_storage_state(state: bool) { fn set_storage_state(state: bool) {
if let Some(storage) = Self::get_storage() { if let Some(storage) = Self::get_storage() {
storage.set(LOCALSTORAGE_KEY, state.to_string().as_str()).ok(); let _ = storage.set(LOCALSTORAGE_KEY, state.to_string().as_str());
} }
} }
} }

View File

@@ -1,5 +1,6 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use wasm_bindgen::JsCast;
use std::collections::HashSet; use std::collections::HashSet;
use icons::{ArrowUpDown, Inbox, Settings2, Play, Square, Trash2, Ellipsis, ArrowUp, ArrowDown, Check, ListFilter}; use icons::{ArrowUpDown, Inbox, Settings2, Play, Square, Trash2, Ellipsis, ArrowUp, ArrowDown, Check, ListFilter};
use crate::store::{get_action_messages, show_toast}; use crate::store::{get_action_messages, show_toast};
@@ -14,7 +15,17 @@ use crate::components::ui::empty::*;
use crate::components::ui::input::Input; use crate::components::ui::input::Input;
use crate::components::ui::multi_select::*; use crate::components::ui::multi_select::*;
use crate::components::ui::dropdown_menu::*; use crate::components::ui::dropdown_menu::*;
use crate::components::ui::alert_dialog::*; use crate::components::ui::alert_dialog::{
AlertDialog,
AlertDialogBody,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
};
use tailwind_fuse::tw_merge; use tailwind_fuse::tw_merge;
const ALL_COLUMNS: [(&str, &str); 8] = [ const ALL_COLUMNS: [(&str, &str); 8] = [
@@ -220,66 +231,78 @@ 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> <div class="flex items-center gap-2">
<DropdownMenuTrigger class="w-[140px] h-9 gap-2"> <DropdownMenu>
<Ellipsis class="size-4" /> <DropdownMenuTrigger class="w-[140px] h-9 gap-2">
{move || format!("Toplu İşlem ({})", selected_count.get())} <Ellipsis class="size-4" />
</DropdownMenuTrigger> {move || format!("Toplu İşlem ({})", selected_count.get())}
<DropdownMenuContent class="w-48"> </DropdownMenuTrigger>
<DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel> <DropdownMenuContent class="w-48">
<DropdownMenuGroup class="mt-2"> <DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel>
<DropdownMenuItem on:click=move |_| bulk_action("start")> <DropdownMenuGroup class="mt-2">
<Play class="mr-2 size-4" /> "Başlat" <DropdownMenuItem on:click=move |_| bulk_action("start")>
</DropdownMenuItem> <Play class="mr-2 size-4" /> "Başlat"
<DropdownMenuItem on:click=move |_| bulk_action("stop")> </DropdownMenuItem>
<Square class="mr-2 size-4" /> "Durdur" <DropdownMenuItem on:click=move |_| bulk_action("stop")>
</DropdownMenuItem> <Square class="mr-2 size-4" /> "Durdur"
</DropdownMenuItem>
<div class="my-1 h-px bg-border" />
<div class="my-1 h-px bg-border" />
<AlertDialog>
<AlertDialogTrigger class="w-full text-left"> // Trigger the hidden AlertDialog from this menu item
<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"> <DropdownMenuItem class="text-destructive focus:bg-destructive/10 cursor-pointer" on:click=move |_| {
<Trash2 class="size-4" /> "Toplu Sil..." if let Some(trigger) = document().get_element_by_id("bulk-delete-trigger") {
</div> let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el: web_sys::HtmlElement| el.click());
</AlertDialogTrigger> }
<AlertDialogContent> }>
<AlertDialogHeader> <Trash2 class="mr-2 size-4" /> "Toplu Sil..."
<AlertDialogTitle class="text-destructive flex items-center gap-2"> </DropdownMenuItem>
<Trash2 class="size-5" /> </DropdownMenuGroup>
"Toplu Silme Onayı" </DropdownMenuContent>
</AlertDialogTitle> </DropdownMenu>
<AlertDialogDescription class="pt-2">
{move || format!("Seçili {} adet torrent silinecek. Lütfen silme yöntemini seçin:", selected_count.get())} // Hidden AlertDialog moved outside the DropdownMenuContent to ensure proper centering
<div class="mt-4 p-3 bg-muted/50 rounded-md text-xs border border-border italic"> <AlertDialog>
"Dikkat: Verilerle birlikte silme işlemi dosyaları diskten de kalıcı olarak kaldıracaktır." <AlertDialogTrigger attr:id="bulk-delete-trigger" class="hidden">""</AlertDialogTrigger>
</div> <AlertDialogContent class="sm:max-w-[425px]">
</AlertDialogDescription> <AlertDialogBody>
</AlertDialogHeader> <AlertDialogHeader class="space-y-3">
<AlertDialogFooter class="gap-2 sm:gap-0"> <AlertDialogTitle class="text-destructive flex items-center gap-2 text-xl">
<div class="flex flex-col sm:flex-row gap-2 w-full justify-end"> <Trash2 class="size-6" />
<AlertDialogClose class="order-3 sm:order-1">"Vazgeç"</AlertDialogClose> "Toplu Silme Onayı"
</AlertDialogTitle>
<AlertDialogDescription class="text-sm leading-relaxed text-left">
{move || format!("Seçili {} adet torrent silinecek. Lütfen silme yöntemini seçin:", selected_count.get())}
<div class="mt-4 p-4 bg-destructive/5 rounded-lg border border-destructive/10 text-xs text-destructive/80 font-medium">
"⚠️ Dikkat: Verilerle birlikte silme işlemi dosyaları diskten de kalıcı olarak kaldıracaktır."
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter class="mt-6">
<div class="flex flex-col-reverse sm:flex-row gap-3 w-full sm:justify-end">
<AlertDialogClose class="sm:flex-1 md:flex-none">"Vazgeç"</AlertDialogClose>
<div class="flex flex-col sm:flex-row gap-2">
<Button <Button
variant=ButtonVariant::Outline variant=ButtonVariant::Secondary
class="order-2 text-foreground" class="w-full sm:w-auto font-medium"
on:click=move |_| bulk_action("delete") on:click=move |_| bulk_action("delete")
> >
"Sadece Listeden Sil" "Sadece Sil"
</Button> </Button>
<Button <Button
variant=ButtonVariant::Destructive variant=ButtonVariant::Destructive
class="order-1" class="w-full sm:w-auto font-bold"
on:click=move |_| bulk_action("delete_with_data") on:click=move |_| bulk_action("delete_with_data")
> >
"Verilerle Birlikte Sil" "Verilerle Sil"
</Button> </Button>
</div> </div>
</AlertDialogFooter> </div>
</AlertDialogContent> </AlertDialogFooter>
</AlertDialog> </AlertDialogBody>
</DropdownMenuGroup> </AlertDialogContent>
</DropdownMenuContent> </AlertDialog>
</DropdownMenu> </div>
</Show> </Show>
// Mobile Sort Menu // Mobile Sort Menu

View File

@@ -6,7 +6,6 @@ pub enum BadgeVariant {
#[default] #[default]
Default, Default,
Secondary, Secondary,
Outline,
Destructive, Destructive,
Success, Success,
Warning, Warning,
@@ -22,7 +21,6 @@ pub fn Badge(
let variant_classes = match variant { let variant_classes = match variant {
BadgeVariant::Default => "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", BadgeVariant::Default => "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
BadgeVariant::Secondary => "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", BadgeVariant::Secondary => "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
BadgeVariant::Outline => "text-foreground",
BadgeVariant::Destructive => "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", BadgeVariant::Destructive => "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
BadgeVariant::Success => "border-transparent bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20", BadgeVariant::Success => "border-transparent bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20",
BadgeVariant::Warning => "border-transparent bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20", BadgeVariant::Warning => "border-transparent bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20",

View File

@@ -209,7 +209,7 @@ pub fn ContextMenuTrigger(
class=trigger_class class=trigger_class
data-name="ContextMenuTrigger" data-name="ContextMenuTrigger"
data-context-trigger=ctx.target_id data-context-trigger=ctx.target_id
on:contextmenu=move |e: web_sys::MouseEvent| { on:contextmenu=move |_e: web_sys::MouseEvent| {
if let Some(cb) = on_open { if let Some(cb) = on_open {
cb.run(()); cb.run(());
} }

View File

@@ -1,6 +1,6 @@
// * Reuse @table.rs // * Reuse @table.rs
pub use crate::components::ui::table::{ pub use crate::components::ui::table::{
Table as DataTable, TableBody as DataTableBody, TableCaption as DataTableCaption, TableCell as DataTableCell, Table as DataTable, TableBody as DataTableBody, TableCell as DataTableCell,
TableFooter as DataTableFooter, TableHead as DataTableHead, TableHeader as DataTableHeader, TableHead as DataTableHead, TableHeader as DataTableHeader,
TableRow as DataTableRow, TableWrapper as DataTableWrapper, TableRow as DataTableRow, TableWrapper as DataTableWrapper,
}; };

View File

@@ -94,8 +94,6 @@ pub fn DropdownMenuAction(
#[prop(optional, into)] class: String, #[prop(optional, into)] class: String,
#[prop(optional, into)] href: Option<String>, #[prop(optional, into)] href: Option<String>,
) -> impl IntoView { ) -> impl IntoView {
let _ctx = expect_context::<DropdownMenuContext>();
let class = tw_merge!( let class = tw_merge!(
"inline-flex gap-2 items-center w-full text-sm text-left transition-colors duration-200 focus:outline-none focus-visible:outline-none text-popover-foreground [&_svg:not([class*='size-'])]:size-4 hover:bg-accent hover:text-accent-foreground", "inline-flex gap-2 items-center w-full text-sm text-left transition-colors duration-200 focus:outline-none focus-visible:outline-none text-popover-foreground [&_svg:not([class*='size-'])]:size-4 hover:bg-accent hover:text-accent-foreground",
class class
@@ -175,17 +173,15 @@ pub enum DropdownMenuAlign {
#[derive(Clone)] #[derive(Clone)]
struct DropdownMenuContext { struct DropdownMenuContext {
target_id: String, target_id: String,
align: DropdownMenuAlign,
} }
#[component] #[component]
pub fn DropdownMenu( pub fn DropdownMenu(
children: Children, children: Children,
#[prop(default = DropdownMenuAlign::default())] align: DropdownMenuAlign,
) -> impl IntoView { ) -> impl IntoView {
let dropdown_target_id = use_random_id_for("dropdown"); let dropdown_target_id = use_random_id_for("dropdown");
let ctx = DropdownMenuContext { target_id: dropdown_target_id.clone(), align }; let ctx = DropdownMenuContext { target_id: dropdown_target_id.clone() };
view! { view! {
<Provider value=ctx> <Provider value=ctx>
@@ -252,12 +248,13 @@ pub enum DropdownMenuPosition {
pub fn DropdownMenuContent( pub fn DropdownMenuContent(
children: Children, children: Children,
#[prop(optional, into)] class: String, #[prop(optional, into)] class: String,
#[prop(default = DropdownMenuAlign::default())] align: DropdownMenuAlign,
#[prop(default = DropdownMenuPosition::default())] position: DropdownMenuPosition, #[prop(default = DropdownMenuPosition::default())] position: DropdownMenuPosition,
) -> impl IntoView { ) -> impl IntoView {
let ctx = expect_context::<DropdownMenuContext>(); let ctx = expect_context::<DropdownMenuContext>();
let base_classes = "z-50 p-1 rounded-md border bg-card shadow-md h-fit fixed transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100"; let base_classes = "z-50 p-1 rounded-md border bg-card shadow-md h-fit fixed transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100";
let width_class = match ctx.align { let width_class = match align {
DropdownMenuAlign::Center => "min-w-full", DropdownMenuAlign::Center => "min-w-full",
_ => "w-[180px]", _ => "w-[180px]",
}; };
@@ -265,7 +262,7 @@ pub fn DropdownMenuContent(
let class = tw_merge!(width_class, base_classes, class); let class = tw_merge!(width_class, base_classes, class);
let target_id_for_script = ctx.target_id.clone(); let target_id_for_script = ctx.target_id.clone();
let align_for_script = match ctx.align { let align_for_script = match align {
DropdownMenuAlign::Start => "start", DropdownMenuAlign::Start => "start",
DropdownMenuAlign::StartOuter => "start-outer", DropdownMenuAlign::StartOuter => "start-outer",
DropdownMenuAlign::End => "end", DropdownMenuAlign::End => "end",
@@ -442,26 +439,6 @@ pub fn DropdownMenuContent(
trigger.addEventListener('click', (e) => {{ trigger.addEventListener('click', (e) => {{
e.stopPropagation(); e.stopPropagation();
// Check if any other dropdown is open
const allDropdowns = document.querySelectorAll('[data-target=\"target__dropdown\"]');
let otherDropdownOpen = false;
allDropdowns.forEach(dd => {{
if (dd !== dropdown && dd.getAttribute('data-state') === 'open') {{
otherDropdownOpen = true;
dd.setAttribute('data-state', 'closed');
dd.style.pointerEvents = 'none';
// Unlock scroll
if (window.ScrollLock) {{
window.ScrollLock.unlock(200);
}}
}}
}});
// If another dropdown was open, just close it and don't open this one
if (otherDropdownOpen) {{
return;
}}
// Normal toggle behavior // Normal toggle behavior
if (isOpen) {{ if (isOpen) {{
closeDropdown(); closeDropdown();
@@ -533,4 +510,4 @@ pub fn DropdownMenuSubItem(children: Children, #[prop(optional, into)] class: St
{children()} {children()}
</li> </li>
} }
} }

View File

@@ -9,7 +9,7 @@ use crate::components::hooks::use_can_scroll_vertical::use_can_scroll_vertical;
use crate::components::hooks::use_random::use_random_id_for; use crate::components::hooks::use_random::use_random_id_for;
// * Reuse @select.rs // * Reuse @select.rs
pub use crate::components::ui::select::{ pub use crate::components::ui::select::{
SelectGroup as MultiSelectGroup, SelectItem as MultiSelectItem, SelectLabel as MultiSelectLabel, SelectGroup as MultiSelectGroup, SelectItem as MultiSelectItem,
}; };
#[derive(Clone, Copy, PartialEq, Eq, Default)] #[derive(Clone, Copy, PartialEq, Eq, Default)]

View File

@@ -31,9 +31,6 @@ pub struct SheetContext {
/* ✨ FUNCTIONS ✨ */ /* ✨ FUNCTIONS ✨ */
/* ========================================================== */ /* ========================================================== */
pub type SheetVariant = ButtonVariant;
pub type SheetSize = ButtonSize;
#[component] #[component]
pub fn Sheet(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { pub fn Sheet(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let sheet_target_id = use_random_id_for("sheet"); let sheet_target_id = use_random_id_for("sheet");
@@ -203,7 +200,7 @@ pub fn SheetContent(
target_id_for_script, target_id_for_script,
)} )}
</script> </script>
}.into_any() }
} }
/* ========================================================== */ /* ========================================================== */

View File

@@ -88,25 +88,7 @@ self.addEventListener("fetch", (event) => {
return; return;
} }
// Special strategy for WASM and Main JS to prevent Preload warnings // Cache-first strategy for static assets (JS, CSS, Images)
if (url.pathname.endsWith(".wasm") || (url.pathname.endsWith(".js") && url.pathname.includes("frontend-"))) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
return caches.match(event.request);
}),
);
return;
}
// Cache-first strategy for other static assets (CSS, Images, etc.)
event.respondWith( event.respondWith(
caches.match(event.request).then((response) => { caches.match(event.request).then((response) => {
return ( return (