Compare commits
10 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8139f9338 | ||
|
|
a3735d0931 | ||
|
|
55f00729ee | ||
|
|
275f4a91b2 | ||
|
|
025a0c4a57 | ||
|
|
b29f9f3cc2 | ||
|
|
feede5c5b4 | ||
|
|
c1306a32a9 | ||
|
|
ed5fba4b46 | ||
|
|
f149603ac8 |
@@ -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" />
|
||||||
|
|||||||
@@ -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! {
|
view! {
|
||||||
<Show when=move || !is_loading.0.get() fallback=|| {
|
<Show when=move || !is_loading.0.get() fallback=|| {
|
||||||
// Standard 1: Always show Dashboard Skeleton
|
// Standard 1: Always show Dashboard Skeleton
|
||||||
view! {
|
view! {
|
||||||
<div class="flex h-screen bg-background text-foreground overflow-hidden">
|
<div class="flex h-screen bg-background text-foreground overflow-hidden">
|
||||||
// Sidebar skeleton
|
// Sidebar skeleton
|
||||||
<div class="w-56 border-r border-border p-4 space-y-4">
|
<div class="w-56 border-r border-border p-4 space-y-4">
|
||||||
<Skeleton class="h-8 w-3/4" />
|
<Skeleton class="h-8 w-3/4" />
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Skeleton class="h-6 w-full" />
|
<Skeleton class="h-6 w-full" />
|
||||||
<Skeleton class="h-6 w-full" />
|
<Skeleton class="h-6 w-full" />
|
||||||
<Skeleton class="h-6 w-4/5" />
|
<Skeleton class="h-6 w-4/5" />
|
||||||
<Skeleton class="h-6 w-full" />
|
<Skeleton class="h-6 w-full" />
|
||||||
<Skeleton class="h-6 w-3/5" />
|
<Skeleton class="h-6 w-3/5" />
|
||||||
<Skeleton class="h-6 w-full" />
|
<Skeleton class="h-6 w-full" />
|
||||||
</div>
|
|
||||||
</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();
|
||||||
|
|||||||
@@ -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(".", ""))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
// Trigger the hidden AlertDialog from this menu item
|
||||||
<AlertDialogTrigger class="w-full text-left">
|
<DropdownMenuItem class="text-destructive focus:bg-destructive/10 cursor-pointer" on:click=move |_| {
|
||||||
<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">
|
if let Some(trigger) = document().get_element_by_id("bulk-delete-trigger") {
|
||||||
<Trash2 class="size-4" /> "Toplu Sil..."
|
let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el: web_sys::HtmlElement| el.click());
|
||||||
</div>
|
}
|
||||||
</AlertDialogTrigger>
|
}>
|
||||||
<AlertDialogContent>
|
<Trash2 class="mr-2 size-4" /> "Toplu Sil..."
|
||||||
<AlertDialogHeader>
|
</DropdownMenuItem>
|
||||||
<AlertDialogTitle class="text-destructive flex items-center gap-2">
|
</DropdownMenuGroup>
|
||||||
<Trash2 class="size-5" />
|
</DropdownMenuContent>
|
||||||
"Toplu Silme Onayı"
|
</DropdownMenu>
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription class="pt-2">
|
// Hidden AlertDialog moved outside the DropdownMenuContent to ensure proper centering
|
||||||
{move || format!("Seçili {} adet torrent silinecek. Lütfen silme yöntemini seçin:", selected_count.get())}
|
<AlertDialog>
|
||||||
<div class="mt-4 p-3 bg-muted/50 rounded-md text-xs border border-border italic">
|
<AlertDialogTrigger attr:id="bulk-delete-trigger" class="hidden">""</AlertDialogTrigger>
|
||||||
"Dikkat: Verilerle birlikte silme işlemi dosyaları diskten de kalıcı olarak kaldıracaktır."
|
<AlertDialogContent class="sm:max-w-[425px]">
|
||||||
</div>
|
<AlertDialogBody>
|
||||||
</AlertDialogDescription>
|
<AlertDialogHeader class="space-y-3">
|
||||||
</AlertDialogHeader>
|
<AlertDialogTitle class="text-destructive flex items-center gap-2 text-xl">
|
||||||
<AlertDialogFooter class="gap-2 sm:gap-0">
|
<Trash2 class="size-6" />
|
||||||
<div class="flex flex-col sm:flex-row gap-2 w-full justify-end">
|
"Toplu Silme Onayı"
|
||||||
<AlertDialogClose class="order-3 sm:order-1">"Vazgeç"</AlertDialogClose>
|
</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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user