Compare commits

..

1 Commits

Author SHA1 Message Date
spinline
8eb34905fc chore: final cleanup of all compiler warnings and unused code across the project
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-13 13:17:22 +03:00
24 changed files with 273 additions and 900 deletions

3
Cargo.lock generated
View File

@@ -320,11 +320,9 @@ dependencies = [
"dotenvy", "dotenvy",
"futures", "futures",
"governor", "governor",
"icons",
"jsonwebtoken", "jsonwebtoken",
"leptos", "leptos",
"leptos_axum", "leptos_axum",
"leptos_ui",
"mime_guess", "mime_guess",
"openssl", "openssl",
"quick-xml", "quick-xml",
@@ -345,7 +343,6 @@ dependencies = [
"tower_governor", "tower_governor",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tw_merge",
"utoipa", "utoipa",
"utoipa-swagger-ui", "utoipa-swagger-ui",
"web-push", "web-push",

View File

@@ -2,9 +2,6 @@
members = ["backend", "frontend", "shared"] members = ["backend", "frontend", "shared"]
resolver = "2" resolver = "2"
[[workspace.metadata.leptos]]
tailwind-input-file = "frontend/input.css"
[profile.release] [profile.release]
# En küçük binary boyutu # En küçük binary boyutu
opt-level = "z" opt-level = "z"

View File

@@ -46,7 +46,4 @@ governor = "0.10.4"
# Leptos # Leptos
leptos = { version = "0.8.15", features = ["nightly"] } leptos = { version = "0.8.15", features = ["nightly"] }
leptos_axum = { version = "0.8.7" } leptos_axum = { version = "0.8.7" }
jsonwebtoken = "9" jsonwebtoken = "9"
tw_merge = { version = "0.1.17", features = ["variant"] }
icons = { version = "0.18.0", features = ["leptos"] }
leptos_ui = "0.3.20"

View File

@@ -3,69 +3,70 @@
:root { :root {
--radius: 0.625rem; --background: 0 0% 100%;
--background: oklch(1 0 0); --foreground: 240 10% 3.9%;
--foreground: oklch(0.145 0 0); --card: 0 0% 100%;
--card: oklch(1 0 0); --card-foreground: 240 10% 3.9%;
--card-foreground: oklch(0.145 0 0); --popover: 0 0% 100%;
--popover: oklch(1 0 0); --popover-foreground: 240 10% 3.9%;
--popover-foreground: oklch(0.145 0 0); --primary: 240 5.9% 10%;
--primary: oklch(0.205 0 0); --primary-foreground: 0 0% 98%;
--primary-foreground: oklch(0.985 0 0); --secondary: 240 4.8% 95.9%;
--secondary: oklch(0.97 0 0); --secondary-foreground: 240 5.9% 10%;
--secondary-foreground: oklch(0.205 0 0); --muted: 240 4.8% 95.9%;
--muted: oklch(0.97 0 0); --muted-foreground: 240 3.8% 46.1%;
--muted-foreground: oklch(0.556 0 0); --accent: 240 4.8% 95.9%;
--accent: oklch(0.97 0 0); --accent-foreground: 240 5.9% 10%;
--accent-foreground: oklch(0.205 0 0); --destructive: 0 84.2% 60.2%;
--destructive: oklch(0.577 0.245 27.325); --destructive-foreground: 0 0% 98%;
--border: oklch(0.922 0 0); --border: 240 5.9% 90%;
--input: oklch(0.922 0 0); --input: 240 5.9% 90%;
--ring: oklch(0.708 0 0); --ring: 240 5.9% 10%;
--radius: 0.5rem;
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: 240 10% 3.9%;
--foreground: oklch(0.985 0 0); --foreground: 0 0% 98%;
--card: oklch(0.205 0 0); --card: 240 10% 3.9%;
--card-foreground: oklch(0.985 0 0); --card-foreground: 0 0% 98%;
--popover: oklch(0.205 0 0); --popover: 240 10% 3.9%;
--popover-foreground: oklch(0.985 0 0); --popover-foreground: 0 0% 98%;
--primary: oklch(0.922 0 0); --primary: 0 0% 98%;
--primary-foreground: oklch(0.205 0 0); --primary-foreground: 240 5.9% 10%;
--secondary: oklch(0.269 0 0); --secondary: 240 3.7% 15.9%;
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: 0 0% 98%;
--muted: oklch(0.269 0 0); --muted: 240 3.7% 15.9%;
--muted-foreground: oklch(0.708 0 0); --muted-foreground: 240 5% 64.9%;
--accent: oklch(0.269 0 0); --accent: 240 3.7% 15.9%;
--accent-foreground: oklch(0.985 0 0); --accent-foreground: 0 0% 98%;
--destructive: oklch(0.704 0.191 22.216); --destructive: 0 62.8% 30.6%;
--border: oklch(1 0 0 / 10%); --destructive-foreground: 0 0% 98%;
--input: oklch(1 0 0 / 15%); --border: 240 3.7% 15.9%;
--ring: oklch(0.556 0 0); --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: hsl(var(--background));
--color-foreground: var(--foreground); --color-foreground: hsl(var(--foreground));
--color-card: var(--card); --color-card: hsl(var(--card));
--color-card-foreground: var(--card-foreground); --color-card-foreground: hsl(var(--card-foreground));
--color-popover: var(--popover); --color-popover: hsl(var(--popover));
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: var(--primary); --color-primary: hsl(var(--primary));
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: var(--secondary); --color-secondary: hsl(var(--secondary));
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: var(--muted); --color-muted: hsl(var(--muted));
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: var(--accent); --color-accent: hsl(var(--accent));
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: var(--destructive); --color-destructive: hsl(var(--destructive));
--color-destructive-foreground: var(--destructive-foreground); --color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: var(--border); --color-border: hsl(var(--border));
--color-input: var(--input); --color-input: hsl(var(--input));
--color-ring: var(--ring); --color-ring: hsl(var(--ring));
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);

View File

@@ -2,8 +2,6 @@ use leptos::prelude::*;
use crate::components::ui::context_menu::{ use crate::components::ui::context_menu::{
ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger,
}; };
use crate::components::ui::button_action::ButtonAction;
use crate::components::ui::button::ButtonVariant;
#[component] #[component]
pub fn TorrentContextMenu( pub fn TorrentContextMenu(
@@ -11,60 +9,37 @@ pub fn TorrentContextMenu(
torrent_hash: String, torrent_hash: String,
on_action: Callback<(String, String)>, on_action: Callback<(String, String)>,
) -> impl IntoView { ) -> impl IntoView {
let hash = torrent_hash.clone();
let on_action_stored = StoredValue::new(on_action);
// Define helper to avoid move issues
let run_action = move |action: &str| {
on_action_stored.get_value().run((action.to_string(), hash.clone()));
};
let hash_c1 = torrent_hash.clone(); let hash_c1 = torrent_hash.clone();
let hash_c2 = torrent_hash.clone(); let hash_c2 = torrent_hash.clone();
let hash_c3 = torrent_hash.clone(); let hash_c3 = torrent_hash.clone();
let hash_c4 = torrent_hash.clone(); let hash_c4 = torrent_hash.clone();
let on_action_stored = StoredValue::new(on_action);
view! { view! {
<ContextMenu> <ContextMenu>
<ContextMenuTrigger> <ContextMenuTrigger>
{children()} {children()}
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent class="w-56 p-1.5"> <ContextMenuContent class="w-48">
<ContextMenuItem on:click={let h = hash_c1; move |_| { <ContextMenuItem on:click={let h = hash_c1; move |_| on_action_stored.get_value().run(("start".to_string(), h.clone()))}>
on_action_stored.get_value().run(("start".to_string(), h.clone()));
crate::components::ui::context_menu::close_context_menu();
}}>
"Başlat" "Başlat"
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem on:click={let h = hash_c2; move |_| { <ContextMenuItem on:click={let h = hash_c2; move |_| on_action_stored.get_value().run(("stop".to_string(), h.clone()))}>
on_action_stored.get_value().run(("stop".to_string(), h.clone()));
crate::components::ui::context_menu::close_context_menu();
}}>
"Durdur" "Durdur"
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem class="text-destructive" on:click={let h = hash_c3; move |_| on_action_stored.get_value().run(("delete".to_string(), h.clone()))}>
<div class="my-1.5 h-px bg-border/50" /> "Sil"
</ContextMenuItem>
// --- Modern Hold-to-Action Buttons --- <ContextMenuItem class="text-destructive font-bold" on:click={let h = hash_c4; move |_| on_action_stored.get_value().run(("delete_with_data".to_string(), h.clone()))}>
<div class="space-y-1"> "Verilerle Birlikte Sil"
<ButtonAction </ContextMenuItem>
variant=ButtonVariant::Ghost
class="w-full justify-start h-8 px-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive transition-none"
hold_duration=800
on_action={let h = hash_c3; move || {
on_action_stored.get_value().run(("delete".to_string(), h.clone()));
crate::components::ui::context_menu::close_context_menu();
}}
>
"Sil (Basılı Tut)"
</ButtonAction>
<ButtonAction
variant=ButtonVariant::Destructive
class="w-full justify-start h-8 px-2 text-xs font-bold"
hold_duration=1200
on_action={let h = hash_c4; move || {
on_action_stored.get_value().run(("delete_with_data".to_string(), h.clone()));
crate::components::ui::context_menu::close_context_menu();
}}
>
"Verilerle Sil (Basılı Tut)"
</ButtonAction>
</div>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
} }

View File

@@ -1,4 +1,4 @@
// use leptos::prelude::*; 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

@@ -6,12 +6,24 @@ pub fn Footer() -> impl IntoView {
let year = chrono::Local::now().format("%Y").to_string(); let year = chrono::Local::now().format("%Y").to_string();
view! { view! {
<footer class="mt-auto pb-6 px-4"> <footer class="mt-auto px-4 py-6 md:px-8">
<Separator class="mb-4 opacity-30" /> <Separator class="mb-6 opacity-50" />
<div class="flex items-center justify-center gap-2 text-[10px] uppercase tracking-widest text-muted-foreground/60 font-medium"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<span>{format!("© {} VibeTorrent", year)}</span> <p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
<span class="size-1 rounded-full bg-muted-foreground/30" /> {format!("© {} VibeTorrent. Tüm hakları saklıdır.", year)}
<span>"v3.0.0-beta"</span> </p>
<div class="flex items-center gap-4 text-sm font-medium text-muted-foreground">
<a
href="https://git.karatatar.com/admin/vibetorrent"
target="_blank"
rel="noreferrer"
class="underline underline-offset-4 hover:text-foreground transition-colors"
>
"Gitea"
</a>
<span class="size-1 rounded-full bg-muted-foreground/30" />
<span class="text-[10px] tracking-widest uppercase opacity-70">"v3.0.0-beta"</span>
</div>
</div> </div>
</footer> </footer>
} }

View File

@@ -3,53 +3,20 @@ use crate::components::layout::sidebar::Sidebar;
use crate::components::layout::toolbar::Toolbar; use crate::components::layout::toolbar::Toolbar;
use crate::components::layout::footer::Footer; use crate::components::layout::footer::Footer;
use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset}; use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset};
use wasm_bindgen::JsCast;
#[component] #[component]
pub fn Protected(children: Children) -> impl IntoView { pub fn Protected(children: Children) -> impl IntoView {
let (collapsed, set_collapsed) = signal(false);
// Responsive Sidebar Logic
Effect::new(move |_| {
let window = web_sys::window().expect("window missing");
// Initial check
let width = window.inner_width().unwrap().as_f64().unwrap_or(1920.0);
if width < 1280.0 {
set_collapsed.set(true);
} else {
set_collapsed.set(false);
}
// Listener
let closure = wasm_bindgen::closure::Closure::<dyn FnMut(_)>::new(move |_: web_sys::Event| {
let window = web_sys::window().expect("window missing");
let width = window.inner_width().unwrap().as_f64().unwrap_or(1920.0);
if width < 1280.0 {
set_collapsed.set(true);
} else {
set_collapsed.set(false);
}
});
let _ = window.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref());
closure.forget(); // Leak memory intentionally for global listener (or store in a cleanup handle if needed, but for layout component it's fine)
});
view! { view! {
<SidenavWrapper attr:style="--sidenav-width:16rem; --sidenav-width-icon:3rem;"> <SidenavWrapper attr:style="--sidenav-width:16rem; --sidenav-width-icon:3rem;">
// Masaüstü Sidenav // Masaüstü Sidenav
<Sidenav <Sidenav>
data_collapsible=crate::components::ui::sidenav::SidenavCollapsible::Icon
data_state=if collapsed.get() { crate::components::ui::sidenav::SidenavState::Collapsed } else { crate::components::ui::sidenav::SidenavState::Expanded }
>
<Sidebar /> <Sidebar />
</Sidenav> </Sidenav>
// İçerik Alanı // İçerik Alanı
<SidenavInset class="flex flex-col h-screen overflow-hidden"> <SidenavInset class="flex flex-col h-screen overflow-hidden">
// Toolbar (Üst Bar) // Toolbar (Üst Bar)
<Toolbar on_toggle_sidebar=Callback::new(move |_| set_collapsed.update(|c| *c = !*c)) /> <Toolbar />
// Ana İçerik // Ana İçerik
<main class="flex-1 overflow-y-auto relative bg-background flex flex-col"> <main class="flex-1 overflow-y-auto relative bg-background flex flex-col">

View File

@@ -87,7 +87,7 @@ pub fn Sidebar() -> impl IntoView {
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
</svg> </svg>
</div> </div>
<div class="grid flex-1 text-left text-sm leading-tight overflow-hidden group-data-[state=Collapsed]:hidden"> <div class="grid flex-1 text-left text-sm leading-tight overflow-hidden">
<span class="truncate font-semibold text-foreground text-base">"VibeTorrent"</span> <span class="truncate font-semibold text-foreground text-base">"VibeTorrent"</span>
<span class="truncate text-[10px] text-muted-foreground opacity-70">"v3.0.0"</span> <span class="truncate text-[10px] text-muted-foreground opacity-70">"v3.0.0"</span>
</div> </div>
@@ -150,28 +150,26 @@ pub fn Sidebar() -> impl IntoView {
<div class="flex flex-col gap-4 p-4"> <div class="flex flex-col gap-4 p-4">
// Push Notification Toggle // Push Notification Toggle
<div class="flex items-center justify-between px-2 py-1 bg-muted/20 rounded-md border border-border/50"> <div class="flex items-center justify-between px-2 py-1 bg-muted/20 rounded-md border border-border/50">
<div class="flex flex-col gap-0.5 group-data-[state=Collapsed]:hidden"> <div class="flex flex-col gap-0.5">
<span class="text-[10px] font-bold uppercase tracking-wider text-foreground/70">"Bildirimler"</span> <span class="text-[10px] font-bold uppercase tracking-wider text-foreground/70">"Bildirimler"</span>
<span class="text-[9px] text-muted-foreground">"Web Push"</span> <span class="text-[9px] text-muted-foreground">"Web Push"</span>
</div> </div>
<div class="group-data-[state=Collapsed]:hidden"> <Switch
<Switch checked=Signal::from(store.push_enabled)
checked=Signal::from(store.push_enabled) on_checked_change=Callback::new(on_push_toggle)
on_checked_change=Callback::new(on_push_toggle) />
/>
</div>
</div> </div>
<div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden group-data-[state=Collapsed]:gap-0 group-data-[state=Collapsed]:justify-center group-data-[state=Collapsed]:p-0 group-data-[state=Collapsed]:border-none group-data-[state=Collapsed]:bg-transparent group-data-[state=Collapsed]:shadow-none"> <div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden">
<div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0 border border-primary-foreground/10"> <div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0 border border-primary-foreground/10">
{first_letter} {first_letter}
</div> </div>
<div class="flex-1 overflow-hidden group-data-[state=Collapsed]:hidden"> <div class="flex-1 overflow-hidden">
<div class="font-medium text-[11px] truncate text-foreground leading-tight">{username}</div> <div class="font-medium text-[11px] truncate text-foreground leading-tight">{username}</div>
<div class="text-[9px] text-muted-foreground truncate opacity-70">"Yönetici"</div> <div class="text-[9px] text-muted-foreground truncate opacity-70">"Yönetici"</div>
</div> </div>
<div class="flex items-center gap-1 group-data-[state=Collapsed]:hidden"> <div class="flex items-center gap-1">
<ThemeToggle /> <ThemeToggle />
<Button <Button
@@ -219,8 +217,8 @@ fn SidebarItem(
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d=icon.clone() /> <path stroke-linecap="round" stroke-linejoin="round" d=icon.clone() />
</svg> </svg>
<span class="flex-1 truncate group-data-[state=Collapsed]:hidden">{label}</span> <span class="flex-1 truncate">{label}</span>
<span class="text-[10px] font-mono opacity-50 group-data-[state=Collapsed]:hidden">{count}</span> <span class="text-[10px] font-mono opacity-50">{count}</span>
</SidenavMenuButton> </SidenavMenuButton>
</SidenavMenuItem> </SidenavMenuItem>
} }

View File

@@ -1,38 +1,24 @@
use leptos::prelude::*; use leptos::prelude::*;
use icons::{PanelLeft, Plus}; use icons::{PanelLeft, Plus};
use crate::components::torrent::add_torrent::AddTorrentDialogContent; use crate::components::torrent::add_torrent::AddTorrentDialogContent;
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize}; use crate::components::ui::button::{ButtonVariant, ButtonSize};
use crate::components::ui::sheet::{Sheet, SheetContent, SheetTrigger, SheetDirection}; use crate::components::ui::sheet::{Sheet, SheetContent, SheetTrigger, SheetDirection};
use crate::components::ui::dialog::{Dialog, DialogContent, DialogTrigger}; use crate::components::ui::dialog::{Dialog, DialogContent, DialogTrigger};
use crate::components::layout::sidebar::Sidebar; use crate::components::layout::sidebar::Sidebar;
#[component] #[component]
pub fn Toolbar( pub fn Toolbar() -> impl IntoView {
on_toggle_sidebar: Callback<()>,
) -> impl IntoView {
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 (Mobil) + Add Torrent // Sol kısım: Menü butonu (Mobil) + Add Torrent
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
// Desktop Toggle
<div class="hidden lg:block"> // --- MOBILE SHEET (SIDEBAR) ---
<Button
variant=ButtonVariant::Ghost
size=ButtonSize::Icon
class="size-9"
on:click=move |_| { on_toggle_sidebar.run(()); }
>
<PanelLeft class="size-5" />
<span class="hidden">"Toggle Sidebar"</span>
</Button>
</div>
// Mobile Toggle (Sheet)
<div class="lg:hidden"> <div class="lg:hidden">
<Sheet> <Sheet>
<SheetTrigger variant=ButtonVariant::Ghost size=ButtonSize::Icon class="size-9"> <SheetTrigger variant=ButtonVariant::Ghost size=ButtonSize::Icon class="size-9">
<PanelLeft class="size-5" /> <PanelLeft class="size-5" />
<span class="hidden">"Open Menu"</span> <span class="hidden">"Menüyü Aç"</span>
</SheetTrigger> </SheetTrigger>
<SheetContent <SheetContent
direction=SheetDirection::Left direction=SheetDirection::Left

View File

@@ -544,14 +544,14 @@ pub fn TorrentTable() -> impl IntoView {
</div> </div>
</div> </div>
<div class="flex items-center justify-between px-2 py-1.5 text-[10px] md:text-[11px] text-muted-foreground bg-muted/20 border rounded-md"> <div class="hidden md:flex items-center justify-between px-2 py-1 text-[11px] text-muted-foreground bg-muted/20 border rounded-md">
<div class="flex gap-3 md:gap-4"> <div class="flex gap-4">
<span>{move || format!("Toplam: {} torrent", filtered_hashes.get().len())}</span> <span>{move || format!("Toplam: {} torrent", filtered_hashes.get().len())}</span>
<Show when=move || has_selection.get()> <Show when=move || has_selection.get()>
<span class="text-primary font-bold">{move || format!("{} seçili", selected_count.get())}</span> <span class="text-primary font-medium">{move || format!("{} torrent seçili", selected_count.get())}</span>
</Show> </Show>
</div> </div>
<div class="opacity-50">"VibeTorrent v3"</div> <div>"VibeTorrent v3"</div>
</div> </div>
</div> </div>
}.into_any() }.into_any()

View File

@@ -1,72 +0,0 @@
use leptos::prelude::*;
use tailwind_fuse::tw_merge;
use crate::components::ui::button::{Button, ButtonVariant};
#[component]
pub fn ButtonAction(
children: Children,
#[prop(into)] on_action: Callback<()>,
#[prop(optional, into)] class: String,
#[prop(default = 1000)] hold_duration: u64,
#[prop(default = ButtonVariant::Default)] variant: ButtonVariant,
) -> impl IntoView {
let is_holding = RwSignal::new(false);
let generation = StoredValue::new(0u64);
let on_down = move |_| {
generation.update_value(|g| *g += 1);
is_holding.set(true);
};
let on_up = move |_| is_holding.set(false);
Effect::new(move |_| {
if is_holding.get() {
let current_gen = generation.get_value();
leptos::task::spawn_local(async move {
gloo_timers::future::TimeoutFuture::new(hold_duration as u32).await;
// Double validation: Is user still holding AND is it the SAME hold attempt?
if is_holding.get_untracked() && generation.get_value() == current_gen {
on_action.run(());
is_holding.set(false);
}
});
}
});
let merged_class = move || tw_merge!(
"relative overflow-hidden transition-all active:scale-[0.98]",
class.clone()
);
view! {
<style>
"@keyframes button-hold-progress {
from { width: 0%; }
to { width: 100%; }
}
.animate-button-hold {
animation: button-hold-progress var(--button-hold-duration) linear forwards;
}"
</style>
<Button
variant=variant
class=merged_class()
attr:style=format!("--button-hold-duration: {}ms", hold_duration)
on:mousedown=on_down
on:mouseup=on_up
on:mouseleave=on_up
on:touchstart=move |_| is_holding.set(true)
on:touchend=move |_| is_holding.set(false)
>
// Progress Overlay
<Show when=move || is_holding.get()>
<div class="absolute inset-0 bg-white/20 dark:bg-black/20 pointer-events-none animate-button-hold" />
</Show>
<span class="relative z-10 flex items-center justify-center gap-2">
{children()}
</span>
</Button>
}
}

View File

@@ -78,6 +78,76 @@ pub fn ContextMenuAction(
} }
} }
#[component]
pub fn ContextMenuHoldAction(
children: Children,
#[prop(into)] on_hold_complete: Callback<()>,
#[prop(optional, into)] class: String,
#[prop(default = 1000)] hold_duration: u64,
) -> impl IntoView {
let is_holding = RwSignal::new(false);
let progress = RwSignal::new(0.0);
let on_mousedown = move |_| {
is_holding.set(true);
progress.set(0.0);
};
let on_mouseup = move |_| {
is_holding.set(false);
progress.set(0.0);
};
Effect::new(move |_| {
if is_holding.get() {
let start_time = js_sys::Date::now();
let duration = hold_duration as f64;
leptos::task::spawn_local(async move {
while is_holding.get_untracked() {
let now = js_sys::Date::now();
let elapsed = now - start_time;
let p = (elapsed / duration).min(1.0);
progress.set(p * 100.0);
if p >= 1.0 {
on_hold_complete.run(());
is_holding.set(false);
close_context_menu();
break;
}
gloo_timers::future::TimeoutFuture::new(16).await; // ~60fps
}
});
}
});
let class = tw_merge!(
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors overflow-hidden",
class
);
view! {
<div
class=class
on:mousedown=on_mousedown
on:mouseup=on_mouseup
on:mouseleave=on_mouseup
on:touchstart=move |_| on_mousedown(web_sys::MouseEvent::new("mousedown").unwrap())
on:touchend=move |_| on_mouseup(web_sys::MouseEvent::new("mouseup").unwrap())
>
// Progress background
<div
class="absolute inset-y-0 left-0 bg-destructive/20 transition-all duration-75 ease-linear pointer-events-none"
style=move || format!("width: {}%;", progress.get())
/>
<span class="relative z-10 flex items-center gap-2 w-full">
{children()}
</span>
</div>
}.into_any()
}
#[derive(Clone)] #[derive(Clone)]
struct ContextMenuContext { struct ContextMenuContext {
target_id: String, target_id: String,
@@ -139,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(());
} }
@@ -327,7 +397,7 @@ pub fn ContextMenuContent(
target_id_for_script, target_id_for_script,
)} )}
</script> </script>
} }.into_any()
} }
#[component] #[component]

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, TableCell as DataTableCell, Table as DataTable, TableBody as DataTableBody, TableCaption as DataTableCaption, TableCell as DataTableCell,
TableHead as DataTableHead, TableHeader as DataTableHeader, TableFooter as DataTableFooter, TableHead as DataTableHead, TableHeader as DataTableHeader,
TableRow as DataTableRow, TableWrapper as DataTableWrapper, TableRow as DataTableRow, TableWrapper as DataTableWrapper,
}; };

View File

@@ -5,7 +5,7 @@ use leptos_ui::clx;
use tw_merge::*; use tw_merge::*;
use crate::components::hooks::use_random::use_random_id_for; use crate::components::hooks::use_random::use_random_id_for;
// pub use crate::components::ui::separator::Separator as DropdownMenuSeparator; pub use crate::components::ui::separator::Separator as DropdownMenuSeparator;
mod components { mod components {
use super::*; use super::*;

View File

@@ -2,7 +2,6 @@ pub mod accordion;
pub mod alert_dialog; pub mod alert_dialog;
pub mod badge; pub mod badge;
pub mod button; pub mod button;
pub mod button_action;
pub mod card; pub mod card;
pub mod checkbox; pub mod checkbox;
pub mod context_menu; pub mod context_menu;

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, SelectGroup as MultiSelectGroup, SelectItem as MultiSelectItem, SelectLabel as MultiSelectLabel,
}; };
#[derive(Clone, Copy, PartialEq, Eq, Default)] #[derive(Clone, Copy, PartialEq, Eq, Default)]

View File

@@ -1,35 +1,20 @@
use leptos::prelude::*; use leptos::prelude::*;
use tw_merge::*; use tailwind_fuse::tw_merge;
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum SeparatorOrientation { #[default] Horizontal, Vertical }
#[component] #[component]
pub fn Separator( pub fn Separator(
#[prop(into, optional)] orientation: Signal<SeparatorOrientation>, #[prop(into, optional)] orientation: Signal<SeparatorOrientation>,
#[prop(into, optional)] class: String, #[prop(into, optional)] class: String,
// children: Children,
) -> impl IntoView { ) -> impl IntoView {
let merged_class = Memo::new(move |_| { let class_signal = move || {
let orientation = orientation.get(); let orient_class = match orientation.get() {
let separator = SeparatorClass { orientation }; SeparatorOrientation::Horizontal => "h-[1px] w-full",
separator.with_class(class.clone()) SeparatorOrientation::Vertical => "h-full w-[1px]",
}); };
tw_merge!("shrink-0 bg-border", orient_class, class.clone())
view! { <div class=merged_class role="separator" /> } };
view! { <div class=class_signal role="none" /> }
} }
/* ========================================================== */
/* 🧬 STRUCT 🧬 */
/* ========================================================== */
#[derive(TwClass, Default)]
#[tw(class = "shrink-0 bg-border")]
pub struct SeparatorClass {
orientation: SeparatorOrientation,
}
#[derive(TwVariant)]
pub enum SeparatorOrientation {
#[tw(default, class = "w-full h-[1px]")]
Default,
#[tw(class = "h-full w-[1px]")]
Vertical,
}

View File

@@ -16,7 +16,7 @@ mod components {
clx! {SheetFooter, footer, "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"} clx! {SheetFooter, footer, "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"}
} }
// pub use components::*; pub use components::*;
/* ========================================================== */ /* ========================================================== */
/* ✨ CONTEXT ✨ */ /* ✨ CONTEXT ✨ */

View File

@@ -1,233 +1,79 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos_router::hooks::use_location; use tw_merge::tw_merge;
use leptos_ui::{clx, variants, void};
mod components { #[derive(Clone, Copy, PartialEq, Eq, Default)]
use super::*; #[allow(dead_code)]
clx! {SidenavWrapper, div, "group/sidenav-wrapper has-data-[variant=Inset]:bg-sidenav flex h-full w-full"} pub enum SidenavState { #[default] Expanded, Collapsed }
// clx! {SidenavInset, div, "bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=Inset]:m-2 md:peer-data-[variant=Inset]:ml-0 md:peer-data-[variant=Inset]:rounded-xl md:peer-data-[variant=Inset]:shadow-sm md:peer-data-[variant=Inset]:peer-data-[state=Collapsed]:ml-2"}
clx! {SidenavInset, div, "bg-background relative flex w-full flex-1 flex-col data-[variant=Inset]:rounded-lg data-[variant=Inset]:border data-[variant=Inset]:border-sidenav-border data-[variant=Inset]:shadow-sm data-[variant=Inset]:m-2"}
// * data-[], not group-data-[]
clx! {SidenavInner, div, "flex flex-col w-full h-full bg-sidenav data-[variant=Floating]:rounded-lg data-[variant=Floating]:border data-[variant=Floating]:border-sidenav-border data-[variant=Floating]:shadow-sm"}
clx! {SidenavHeader, div, "flex flex-col gap-2 p-2"}
clx! {SidenavMenu, ul, "flex flex-col gap-1 w-full min-w-0"}
clx! {SidenavMenuSub, ul, "border-sidenav-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=Icon]:hidden"}
clx! {SidenavMenuItem, li, "relative group/menu-item"}
clx! {SidenavContent, div, "scrollbar__on_hover", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=Icon]:overflow-hidden"}
clx! {SidenavGroup, div, "flex relative flex-col p-2 w-full min-w-0"}
clx! {SidenavGroupContent, div, "w-full text-sm"}
clx! {SidenavGroupLabel, div, "text-sidenav-foreground/70 ring-sidenav-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=Icon]:-mt-8 group-data-[collapsible=Icon]:opacity-0"}
clx! {SidenavFooter, footer, "flex flex-col gap-2 p-2"}
// Button "More"
clx! {DropdownMenuTriggerEllipsis, button, "text-sidenav-foreground ring-sidenav-ring hover:bg-sidenav-accent hover:text-sidenav-accent-foreground peer-hover/menu-button:text-sidenav-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 group-data-[collapsible=Icon]:hidden peer-data-[active=true]/menu-button:text-sidenav-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0"}
void! {SidenavInput, input, #[derive(Clone)]
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", pub struct SidenavContext {
"focus-visible:border-ring focus-visible:ring-ring/50", pub state: RwSignal<SidenavState>,
"focus-visible:ring-2", // TODO. Port tw_merge to Tailwind V4.
// "focus-visible:ring-[3px]", // TODO. Port tw_merge to Tailwind V4.
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"read-only:bg-muted",
// Specific to Sidenav
"w-full h-8 shadow-none bg-background"
}
} }
pub use components::*; #[component]
pub fn SidenavWrapper(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
/* ========================================================== */ let state = RwSignal::new(SidenavState::Expanded);
/* ✨ FUNCTIONS ✨ */ provide_context(SidenavContext { state });
/* ========================================================== */ let class = tw_merge!("flex min-h-screen w-full bg-background", class);
view! { <div class=class>{children()}</div> }
}
#[component] #[component]
pub fn SidenavLink( pub fn Sidenav(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
children: Children, let ctx = expect_context::<SidenavContext>();
#[prop(into)] href: String, let class_signal = move || {
#[prop(optional, into)] class: String, let width_class = match ctx.state.get() {
) -> impl IntoView { SidenavState::Expanded => "w-[var(--sidenav-width)]",
let merged_class = tw_merge!( SidenavState::Collapsed => "w-[var(--sidenav-width-icon)]",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left outline-hidden ring-sidenav-ring transition-[width,height,padding] focus-visible:ring-2 active:bg-sidenav-accent active:text-sidenav-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidenav=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-[current=page]:bg-sidenav-accent aria-[current=page]:font-semibold aria-[current=page]:text-sidenav-accent-foreground data-[state=open]:hover:bg-sidenav-accent data-[state=open]:hover:text-sidenav-accent-foreground group-data-[collapsible=Icon]:size-8! group-data-[collapsible=Icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 hover:bg-sidenav-accent hover:text-sidenav-accent-foreground h-8 text-sm", };
class tw_merge!(
); "hidden md:flex flex-col border-r bg-card transition-all duration-300",
width_class,
let location = use_location(); class.clone()
)
// Check if the link is active based on current path
let href_clone = href.clone();
let is_active = move || {
let path = location.pathname.get();
path == href_clone || path.starts_with(&format!("{}/", href_clone))
}; };
view! { <aside class=class_signal>{children()}</aside> }
let aria_current = move || if is_active() { "page" } else { "false" };
view! {
<a data-name="SidenavLink" class=merged_class href=href aria-current=aria_current>
{children()}
</a>
}
}
variants! {
SidenavMenuButton {
base: "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidenav-ring transition-[width,height,padding] hover:bg-sidenav-accent hover:text-sidenav-accent-foreground focus-visible:ring-2 active:bg-sidenav-accent active:text-sidenav-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidenav=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-[current=page]:bg-sidenav-accent aria-[current=page]:font-medium aria-[current=page]:text-sidenav-accent-foreground data-[state=open]:hover:bg-sidenav-accent data-[state=open]:hover:text-sidenav-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=Icon]:size-8! group-data-[collapsible=Icon]:p-0! [&>svg]:stroke-2 aria-[current=page]:[&>svg]:stroke-[2.7]",
variants: {
variant: {
Default: "hover:bg-sidenav-accent hover:text-sidenav-accent-foreground", // Already in base
Outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidenav-border))] hover:bg-sidenav-accent hover:text-sidenav-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidenav-accent))]",
},
size: {
Default: "h-8 text-sm",
Sm: "h-7 text-xs",
Lg: "h-12",
}
},
component: {
element: button,
support_href: true,
support_aria_current: true
}
}
}
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display, strum::IntoStaticStr)]
pub enum SidenavVariant {
#[default]
Sidenav,
Floating,
Inset,
}
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display)]
pub enum SidenavSide {
#[default]
Left,
Right,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, strum::Display)]
pub enum SidenavCollapsible {
#[default]
Offcanvas,
None,
Icon,
} }
#[component] #[component]
pub fn Sidenav( pub fn SidenavInset(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
#[prop(into, optional)] class: String, let class = tw_merge!("flex flex-col flex-1 min-w-0", class);
#[prop(default = SidenavVariant::default())] variant: SidenavVariant, view! { <main class=class>{children()}</main> }
#[prop(default = SidenavState::default())] data_state: SidenavState, }
#[prop(default = SidenavSide::default())] data_side: SidenavSide,
#[prop(default = SidenavCollapsible::default())] data_collapsible: SidenavCollapsible, #[component] pub fn SidenavHeader(children: Children) -> impl IntoView { view! { <div class="flex flex-col">{children()}</div> } }
#[component] pub fn SidenavContent(children: Children) -> impl IntoView { view! { <div class="flex-1 overflow-auto">{children()}</div> } }
#[component] pub fn SidenavFooter(children: Children) -> impl IntoView { view! { <div class="mt-auto">{children()}</div> } }
#[component] pub fn SidenavGroup(children: Children) -> impl IntoView { view! { <div class="px-2 py-2">{children()}</div> } }
#[component] pub fn SidenavGroupLabel(children: Children) -> impl IntoView { view! { <div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">{children()}</div> } }
#[component] pub fn SidenavGroupContent(children: Children) -> impl IntoView { view! { <div class="space-y-1">{children()}</div> } }
#[component] pub fn SidenavMenu(children: Children) -> impl IntoView { view! { <nav class="grid gap-1">{children()}</nav> } }
#[component] pub fn SidenavMenuItem(children: Children) -> impl IntoView { view! { <div>{children()}</div> } }
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum SidenavMenuButtonVariant { #[default] Default, Outline }
#[component]
pub fn SidenavMenuButton(
children: Children, children: Children,
#[prop(into, optional)] variant: Signal<SidenavMenuButtonVariant>,
#[prop(into, optional)] class: Signal<String>,
) -> impl IntoView { ) -> impl IntoView {
view! { let class_signal = move || {
{if data_collapsible == SidenavCollapsible::None { let variant_class = if variant.get() == SidenavMenuButtonVariant::Outline {
view! { "border border-input bg-background shadow-xs"
<aside } else {
data-name="Sidenav" ""
class=tw_merge!( };
"flex flex-col h-full bg-sidenav text-sidenav-foreground w-(--sidenav-width)", class.clone() tw_merge!(
) "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
> variant_class,
{children()} class.get()
</aside> )
} };
.into_any() view! { <button class=class_signal>{children()}</button> }
} else {
view! {
<aside
data-name="Sidenav"
data-sidenav=data_state.to_string()
data-side=data_side.to_string()
data-collapsible=data_collapsible.to_string()
class="hidden md:block group peer text-sidenav-foreground group-data-[collapsible=Offcanvas]:data-[state=Collapsed]:hidden"
>
// * SidenavGap: This is what handles the sidenav gap on desktop
<div
data-name="SidenavGap"
class=tw_merge!(
"relative w-(--sidenav-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=Offcanvas]:w-0",
"group-data-[side=Right]:rotate-180",
match variant {
SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-(--sidenav-width-icon)",
SidenavVariant::Floating | SidenavVariant::Inset =>
"group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4)))]",
}
)
/>
<div
data-name="SidenavContainer"
class=tw_merge!(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidenav-width) transition-[left,right,width] duration-200 ease-linear md:flex",
class,
match data_side {
SidenavSide::Left => "left-0 group-data-[collapsible=Offcanvas]:left-[calc(var(--sidenav-width)*-1)]",
SidenavSide::Right => "right-0 group-data-[collapsible=Offcanvas]:right-[calc(var(--sidenav-width)*-1)]"
},
match variant {
SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-(--sidenav-width-icon) group-data-[side=Left]:border-r group-data-[side=Right]:border-l",
SidenavVariant::Floating | SidenavVariant::Inset =>
"p-2 group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4))+2px)]",
},
)
>
// * Act as a Sidenav for the onclick trigger to work with nested Sidenavs.
<SidenavInner attr:data-sidenav="Sidenav" attr:data-variant=variant.to_string()>
{children()}
<SidenavToggleRail />
</SidenavInner>
</div>
</aside>
}
.into_any()
}}
}
} }
/* ========================================================== */ #[component] pub fn SidenavLink(children: Children, #[prop(into)] href: String) -> impl IntoView {
/* ✨ FUNCTIONS ✨ */ view! { <a href=href class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent">{children()}</a> }
/* ========================================================== */
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display)]
pub enum SidenavState {
#[default]
Expanded,
Collapsed,
} }
const ONCLICK_TRIGGER: &str = "document.querySelector('[data-name=\"Sidenav\"]').setAttribute('data-state', document.querySelector('[data-name=\"Sidenav\"]').getAttribute('data-state') === 'Collapsed' ? 'Expanded' : 'Collapsed')";
#[component]
pub fn SidenavTrigger(children: Children) -> impl IntoView {
view! {
// TODO. Use Button.
<button
onclick=ONCLICK_TRIGGER
data-name="SidenavTrigger"
class="inline-flex gap-2 justify-center items-center -ml-1 text-sm font-medium whitespace-nowrap rounded-md transition-all outline-none disabled:opacity-50 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 aria-invalid:ring-destructive/20 aria-invalid:border-destructive size-7 dark:aria-invalid:ring-destructive/40 dark:hover:bg-accent/50 hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
>
{children()}
</button>
}
}
#[component]
fn SidenavToggleRail() -> impl IntoView {
view! {
<button
data-name="SidenavToggleRail"
aria-label="Toggle Sidenav"
tabindex="-1"
onclick=ONCLICK_TRIGGER
class="hidden absolute inset-y-0 z-20 w-4 transition-all ease-linear -translate-x-1/2 sm:flex group-data-[side=Left]:-right-4 group-data-[side=Right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] in-data-[side=Left]:cursor-w-resize in-data-[side=Right]:cursor-e-resize [[data-side=Left][data-state=Collapsed]_&]:cursor-e-resize [[data-side=Right][data-state=Collapsed]_&]:cursor-w-resize group-data-[collapsible=Offcanvas]:translate-x-0 group-data-[collapsible=Offcanvas]:after:left-full [[data-side=Left][data-collapsible=Offcanvas]_&]:-right-2 [[data-side=Right][data-collapsible=Offcanvas]_&]:-left-0eft-2 hover:after:bg-sidenav-border hover:group-data-[collapsible=Offcanvas]:bg-sidenav"
/>
}
}

View File

@@ -153,8 +153,7 @@ pub fn provide_torrent_store() {
pub async fn is_push_subscribed() -> Result<bool, String> { pub async fn is_push_subscribed() -> Result<bool, String> {
let window = web_sys::window().ok_or("no window")?; let window = web_sys::window().ok_or("no window")?;
let navigator = window.navigator(); let sw_container = window.navigator().service_worker();
let sw_container = navigator.service_worker();
let registration = wasm_bindgen_futures::JsFuture::from(sw_container.ready().map_err(|e| format!("{:?}", e))?) let registration = wasm_bindgen_futures::JsFuture::from(sw_container.ready().map_err(|e| format!("{:?}", e))?)
.await .await
@@ -172,8 +171,7 @@ pub async fn is_push_subscribed() -> Result<bool, String> {
pub async fn subscribe_to_push_notifications() { pub async fn subscribe_to_push_notifications() {
let window = web_sys::window().expect("no window"); let window = web_sys::window().expect("no window");
let navigator = window.navigator(); let sw_container = window.navigator().service_worker();
let sw_container = navigator.service_worker();
let registration = match wasm_bindgen_futures::JsFuture::from(sw_container.ready().expect("sw not ready")).await { let registration = match wasm_bindgen_futures::JsFuture::from(sw_container.ready().expect("sw not ready")).await {
Ok(reg) => reg.dyn_into::<web_sys::ServiceWorkerRegistration>().expect("not a reg"), Ok(reg) => reg.dyn_into::<web_sys::ServiceWorkerRegistration>().expect("not a reg"),
@@ -181,8 +179,7 @@ pub async fn subscribe_to_push_notifications() {
}; };
// 1. Get Public Key from Backend // 1. Get Public Key from Backend
let public_key_res: Result<String, _> = shared::server_fns::push::get_public_key().await; let public_key = match shared::server_fns::push::get_public_key().await {
let public_key = match public_key_res {
Ok(key) => key, Ok(key) => key,
Err(e) => { log::error!("Failed to get public key: {:?}", e); return; } Err(e) => { log::error!("Failed to get public key: {:?}", e); return; }
}; };
@@ -192,7 +189,7 @@ pub async fn subscribe_to_push_notifications() {
let key_array = js_sys::Uint8Array::from(&decoded_key[..]); let key_array = js_sys::Uint8Array::from(&decoded_key[..]);
// 3. Prepare Options // 3. Prepare Options
let options = web_sys::PushSubscriptionOptionsInit::new(); let mut options = web_sys::PushSubscriptionOptionsInit::new();
options.set_user_visible_only(true); options.set_user_visible_only(true);
options.set_application_server_key(&key_array.into()); options.set_application_server_key(&key_array.into());

372
package-lock.json generated
View File

@@ -1,372 +0,0 @@
{
"name": "vibetorrent-v3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@tailwindcss/cli": "^4.1.18",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz",
"integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==",
"license": "MIT",
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"enhanced-resolve": "^5.18.3",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.1.18"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
"integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
}
}
}
}

View File

@@ -1,8 +0,0 @@
{
"type": "module",
"dependencies": {
"@tailwindcss/cli": "^4.1.18",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0"
}
}

View File

@@ -1,2 +0,0 @@
base_color = "neutral"
base_path_components = "backend/src/components"