Compare commits

..

11 Commits

Author SHA1 Message Date
spinline
8fc3571848 fix: restore missing BASE64 import
All checks were successful
Build MIPS Binary / build (push) Successful in 1m51s
2026-02-14 02:23:08 +03:00
spinline
792b6bc97b fix: resolve toolbar syntax error and unused imports
Some checks failed
Build MIPS Binary / build (push) Failing after 34s
2026-02-14 02:20:53 +03:00
spinline
6efa6cd2d9 fix: resolve unused variable warning
Some checks failed
Build MIPS Binary / build (push) Failing after 45s
2026-02-14 02:18:45 +03:00
spinline
89caa17b92 fix: correct malformed import in multi_select.rs
Some checks failed
Build MIPS Binary / build (push) Failing after 46s
2026-02-14 02:16:53 +03:00
spinline
47bb60d7d8 fix: explicit type fixes for toolbar and multi_select
Some checks failed
Build MIPS Binary / build (push) Failing after 45s
2026-02-14 02:14:40 +03:00
spinline
7730250b61 fix: resolve updated compilation errors
Some checks failed
Build MIPS Binary / build (push) Failing after 45s
2026-02-14 02:11:58 +03:00
spinline
73d111124a refactor: simplify toolbar toggle button structure
Some checks failed
Build MIPS Binary / build (push) Failing after 32s
2026-02-14 02:10:43 +03:00
spinline
670c5a653b fix: resolve type inference error in toolbar callback
Some checks failed
Build MIPS Binary / build (push) Failing after 40s
2026-02-14 02:09:12 +03:00
spinline
9394a56e7d fix: compile error in protected layout
Some checks failed
Build MIPS Binary / build (push) Failing after 45s
2026-02-14 02:07:43 +03:00
spinline
105388eec3 feat: refine responsive sidebar behavior with auto-collapse
Some checks failed
Build MIPS Binary / build (push) Failing after 30s
2026-02-14 02:06:20 +03:00
spinline
f5d9cb642c feat: implement collapsible sidebar
Some checks failed
Build MIPS Binary / build (push) Failing after 34s
2026-02-14 01:57:40 +03:00
12 changed files with 333 additions and 165 deletions

View File

@@ -1,4 +1,4 @@
use crate::components::layout::protected::ProtectedLayout;
use crate::components::layout::protected::Protected;
use crate::components::ui::skeleton::Skeleton;
use crate::components::torrent::table::TorrentTable;
use crate::components::auth::login::Login;
@@ -50,11 +50,13 @@ fn InnerApp() -> impl IntoView {
Effect::new(move |_| {
spawn_local(async move {
log::info!("App initialization started...");
gloo_console::log!("APP INIT: Checking setup status...");
// Check if setup is needed via Server Function
match shared::server_fns::auth::get_setup_status().await {
Ok(status) => {
if !status.completed {
log::info!("Setup not completed");
needs_setup.1.set(true);
is_loading.1.set(false);
return;
@@ -66,6 +68,7 @@ fn InnerApp() -> impl IntoView {
// Check authentication via GetUser Server Function
match shared::server_fns::auth::get_user().await {
Ok(Some(user_info)) => {
log::info!("Authenticated as {}", user_info.username);
if let Some(s) = store {
s.user.set(Some(user_info.username));
}
@@ -80,6 +83,7 @@ fn InnerApp() -> impl IntoView {
}
is_loading.1.set(false);
crate::store::toast_success("VibeTorrent'e Hoşgeldiniz");
});
});
@@ -95,14 +99,11 @@ fn InnerApp() -> impl IntoView {
}
});
let is_loading_val = move || is_loading.0.get();
let authenticated_val = move || is_authenticated.0.get();
view! {
<div class="relative w-full h-screen" style="height: 100dvh;">
<Routes fallback=|| view! { <div class="p-4">"404 Not Found"</div> }.into_any()>
<Routes fallback=|| view! { <div class="p-4">"404 Not Found"</div> }>
<Route path=leptos_router::path!("/login") view=move || {
let authenticated = authenticated_val();
let authenticated = is_authenticated.0.get();
let setup_needed = needs_setup.0.get();
Effect::new(move |_| {
@@ -110,75 +111,93 @@ fn InnerApp() -> impl IntoView {
let navigate = use_navigate();
navigate("/setup", Default::default());
} else if authenticated {
log::info!("Already authenticated, redirecting to home");
let navigate = use_navigate();
navigate("/", Default::default());
}
});
view! { <Login /> }.into_any()
view! { <Login /> }
} />
<Route path=leptos_router::path!("/setup") view=move || {
Effect::new(move |_| {
if authenticated_val() {
if is_authenticated.0.get() {
let navigate = use_navigate();
navigate("/", Default::default());
}
});
view! { <Setup /> }.into_any()
view! { <Setup /> }
} />
<Route path=leptos_router::path!("/") view=move || {
let navigate = use_navigate();
Effect::new(move |_| {
if !is_loading_val() {
if !is_loading.0.get() {
if needs_setup.0.get() {
log::info!("Setup not completed, redirecting to setup");
navigate("/setup", Default::default());
} else if !authenticated_val() {
} else if !is_authenticated.0.get() {
log::info!("Not authenticated, redirecting to login");
navigate("/login", Default::default());
}
}
});
view! {
<Show when=move || !is_loading_val() fallback=|| {
<Show when=move || !is_loading.0.get() fallback=|| {
// Standard 1: Always show Dashboard Skeleton
view! {
<div class="flex h-screen bg-background text-foreground overflow-hidden">
// Sidebar skeleton
<div class="w-56 border-r border-border p-4 space-y-4">
<Skeleton class="h-8 w-3/4" />
<div class="space-y-2">
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-4/5" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-3/5" />
<Skeleton class="h-6 w-full" />
</div>
</div>
// Main content skeleton
<div class="flex-1 flex flex-col 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>
}.into_any()
}>
<Show when=move || authenticated_val() fallback=|| ()>
<ProtectedLayout>
<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>
</ProtectedLayout>
</Protected>
</Show>
</Show>
}.into_any()
}/>
<Route path=leptos_router::path!("/settings") view=move || {
let authenticated = authenticated_val();
let authenticated = is_authenticated.0.get();
Effect::new(move |_| {
if !authenticated {
let navigate = use_navigate();
@@ -187,14 +206,14 @@ fn InnerApp() -> impl IntoView {
});
view! {
<Show when=move || !is_loading_val() fallback=|| ()>
<Show when=move || !is_loading.0.get() fallback=|| ()>
<Show when=move || authenticated fallback=|| ()>
<ProtectedLayout>
<Protected>
<div class="p-4">"Settings Page (Coming Soon)"</div>
</ProtectedLayout>
</Protected>
</Show>
</Show>
}.into_any()
}
}/>
</Routes>
</div>

View File

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

View File

@@ -1,25 +1,62 @@
use leptos::prelude::*;
use crate::components::layout::sidebar::Sidebar;
use crate::components::layout::toolbar::Toolbar;
use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset, SidenavState, SidenavCollapsible};
use crate::components::layout::footer::Footer;
use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset};
use wasm_bindgen::JsCast;
#[component]
pub fn ProtectedLayout(children: Children) -> impl IntoView {
let sidenav_state = RwSignal::new(SidenavState::Expanded);
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! {
<SidenavWrapper attr:style="--sidenav-width:16rem; --sidenav-width-icon:3.5rem;">
<SidenavWrapper attr:style="--sidenav-width:16rem; --sidenav-width-icon:3rem;">
// Masaüstü Sidenav
<Sidenav
data_state=Signal::from(sidenav_state)
data_collapsible=SidenavCollapsible::Icon
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 />
</Sidenav>
// İçerik Alanı
<SidenavInset class="flex flex-col h-screen overflow-hidden">
<Toolbar sidenav_state=sidenav_state />
<main class="flex-1 overflow-auto bg-muted/30">
{children()}
// Toolbar (Üst Bar)
<Toolbar on_toggle_sidebar=Callback::new(move |_| set_collapsed.update(|c| *c = !*c)) />
// Ana İçerik
<main class="flex-1 overflow-y-auto relative bg-background flex flex-col">
<div class="flex-1">
{children()}
</div>
<Footer />
</main>
</SidenavInset>
</SidenavWrapper>

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" />
</svg>
</div>
<div class="grid flex-1 text-left text-sm leading-tight overflow-hidden">
<div class="grid flex-1 text-left text-sm leading-tight overflow-hidden group-data-[state=Collapsed]:hidden">
<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>
</div>
@@ -150,26 +150,28 @@ pub fn Sidebar() -> impl IntoView {
<div class="flex flex-col gap-4 p-4">
// 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 flex-col gap-0.5">
<div class="flex flex-col gap-0.5 group-data-[state=Collapsed]:hidden">
<span class="text-[10px] font-bold uppercase tracking-wider text-foreground/70">"Bildirimler"</span>
<span class="text-[9px] text-muted-foreground">"Web Push"</span>
</div>
<Switch
checked=Signal::from(store.push_enabled)
on_checked_change=Callback::new(on_push_toggle)
/>
<div class="group-data-[state=Collapsed]:hidden">
<Switch
checked=Signal::from(store.push_enabled)
on_checked_change=Callback::new(on_push_toggle)
/>
</div>
</div>
<div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden">
<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="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}
</div>
<div class="flex-1 overflow-hidden">
<div class="flex-1 overflow-hidden group-data-[state=Collapsed]:hidden">
<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>
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 group-data-[state=Collapsed]:hidden">
<ThemeToggle />
<Button
@@ -217,8 +219,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">
<path stroke-linecap="round" stroke-linejoin="round" d=icon.clone() />
</svg>
<span class="flex-1 truncate">{label}</span>
<span class="text-[10px] font-mono opacity-50">{count}</span>
<span class="flex-1 truncate group-data-[state=Collapsed]:hidden">{label}</span>
<span class="text-[10px] font-mono opacity-50 group-data-[state=Collapsed]:hidden">{count}</span>
</SidenavMenuButton>
</SidenavMenuItem>
}

View File

@@ -1,24 +1,69 @@
use leptos::prelude::*;
use crate::components::ui::sidenav::{SidenavTrigger, SidenavState};
use crate::components::torrent::add_torrent::AddTorrent;
use icons::{PanelLeft, Plus};
use crate::components::torrent::add_torrent::AddTorrentDialogContent;
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
use crate::components::ui::sheet::{Sheet, SheetContent, SheetTrigger, SheetDirection};
use crate::components::ui::dialog::{Dialog, DialogContent, DialogTrigger};
use crate::components::layout::sidebar::Sidebar;
#[component]
pub fn Toolbar(
sidenav_state: RwSignal<SidenavState>
on_toggle_sidebar: Callback<()>,
) -> impl IntoView {
let header_view = view! {
<header class="h-14 border-b bg-background/95 backdrop-blur-sm flex items-center justify-between px-4 sticky top-0 z-30">
<div class="flex items-center gap-4">
<SidenavTrigger data_state=sidenav_state />
<div class="h-4 w-px bg-border hidden md:block"></div>
<h2 class="text-sm font-semibold tracking-tight">"Torrents"</h2>
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);">
// Sol kısım: Menü butonu (Mobil) + Add Torrent
<div class="flex items-center gap-3">
// Desktop Toggle
<div class="hidden lg:block">
<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">
<Sheet>
<SheetTrigger variant=ButtonVariant::Ghost size=ButtonSize::Icon class="size-9">
<PanelLeft class="size-5" />
<span class="hidden">"Open Menu"</span>
</SheetTrigger>
<SheetContent
direction=SheetDirection::Left
class="p-0 w-[18rem] bg-card border-r border-border"
hide_close_button=true
>
<div class="flex flex-col h-full overflow-hidden">
<Sidebar />
</div>
</SheetContent>
</Sheet>
</div>
<Dialog>
<DialogTrigger
variant=ButtonVariant::Default
class="gap-2"
>
<Plus class="w-4 h-4 md:w-5 md:h-5" />
<span class="hidden sm:inline">"Add Torrent"</span>
<span class="sm:hidden">"Add"</span>
</DialogTrigger>
<DialogContent id="add-torrent-dialog" class="sm:max-w-[425px]">
<AddTorrentDialogContent />
</DialogContent>
</Dialog>
</div>
<div class="flex items-center gap-2">
<AddTorrent />
// Sağ kısım boş
<div class="flex flex-1 items-center justify-end gap-2">
</div>
</header>
};
header_view
</div>
}
}

View File

@@ -3,26 +3,10 @@ use leptos::task::spawn_local;
use wasm_bindgen::JsCast;
use crate::components::ui::input::{Input, InputType};
use crate::api;
use crate::components::ui::button::{Button, ButtonVariant};
use crate::components::ui::button::Button;
use crate::components::ui::dialog::{
Dialog, DialogTrigger, DialogContent, DialogBody, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose
DialogBody, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose
};
use icons::Play;
#[component]
pub fn AddTorrent() -> impl IntoView {
view! {
<Dialog>
<DialogTrigger variant=ButtonVariant::Default class="gap-2">
<Play class="size-4" />
<span class="hidden sm:inline">"Add Torrent"</span>
</DialogTrigger>
<DialogContent id="add-torrent-dialog">
<AddTorrentDialogContent />
</DialogContent>
</Dialog>
}
}
#[component]
pub fn AddTorrentDialogContent() -> impl IntoView {

View File

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

View File

@@ -5,7 +5,7 @@ use leptos_ui::clx;
use tw_merge::*;
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 {
use super::*;

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;
// * Reuse @select.rs
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)]

View File

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

View File

@@ -1,10 +1,13 @@
use leptos::prelude::*;
use leptos_router::hooks::use_location;
use leptos_ui::{clx, variants, void};
mod components {
use super::*;
clx! {SidenavWrapper, div, "group/sidenav-wrapper has-data-[variant=Inset]:bg-sidenav flex h-full w-full"}
// 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"}
@@ -15,20 +18,82 @@ mod components {
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,
"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",
"focus-visible:border-ring focus-visible:ring-ring/50",
"focus-visible:ring-2",
"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::*;
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
#[component]
pub fn SidenavLink(
children: Children,
#[prop(into)] href: String,
#[prop(optional, into)] class: String,
) -> impl IntoView {
let merged_class = tw_merge!(
"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
);
let location = use_location();
// 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))
};
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]
@@ -52,6 +117,84 @@ pub enum SidenavCollapsible {
Icon,
}
#[component]
pub fn Sidenav(
#[prop(into, optional)] class: String,
#[prop(default = SidenavVariant::default())] variant: SidenavVariant,
#[prop(default = SidenavState::default())] data_state: SidenavState,
#[prop(default = SidenavSide::default())] data_side: SidenavSide,
#[prop(default = SidenavCollapsible::default())] data_collapsible: SidenavCollapsible,
children: Children,
) -> impl IntoView {
view! {
{if data_collapsible == SidenavCollapsible::None {
view! {
<aside
data-name="Sidenav"
class=tw_merge!(
"flex flex-col h-full bg-sidenav text-sidenav-foreground w-(--sidenav-width)", class.clone()
)
>
{children()}
</aside>
}
.into_any()
} 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()
}}
}
}
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display)]
pub enum SidenavState {
#[default]
@@ -59,94 +202,32 @@ pub enum SidenavState {
Collapsed,
}
#[component]
pub fn Sidenav(
#[prop(into, optional)] class: String,
#[prop(default = SidenavVariant::default())] variant: SidenavVariant,
#[prop(into)] data_state: Signal<SidenavState>,
#[prop(default = SidenavSide::default())] data_side: SidenavSide,
#[prop(default = SidenavCollapsible::default())] data_collapsible: SidenavCollapsible,
children: Children,
) -> impl IntoView {
view! {
<aside
data-name="Sidenav"
data-state=move || data_state.get().to_string()
data-side=data_side.to_string()
data-collapsible=data_collapsible.to_string()
class="hidden md:block group peer text-sidenav-foreground h-full"
>
<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-[state=Collapsed]:w-(--sidenav-width-icon)",
match variant {
SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:w-(--sidenav-width-icon)",
SidenavVariant::Floating | SidenavVariant::Inset =>
"group-data-[collapsible=Icon]: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)]"
},
"group-data-[state=Collapsed]:w-(--sidenav-width-icon)",
match variant {
SidenavVariant::Sidenav => "group-data-[side=Left]:border-r group-data-[side=Right]:border-l",
SidenavVariant::Floating | SidenavVariant::Inset => "p-2",
},
)
>
<SidenavInner attr:data-sidenav="Sidenav" attr:data-variant=variant.to_string()>
{children()}
</SidenavInner>
</div>
</aside>
}
}
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-[state=Collapsed]:size-8! group-data-[state=Collapsed]:p-0! [&>svg]:stroke-2 aria-[current=page]:[&>svg]:stroke-[2.7]",
variants: {
variant: {
Default: "hover:bg-sidenav-accent hover:text-sidenav-accent-foreground",
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
}
}
}
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(
#[prop(into)] data_state: RwSignal<SidenavState>,
#[prop(optional, into)] class: String,
) -> impl IntoView {
pub fn SidenavTrigger(children: Children) -> impl IntoView {
view! {
// TODO. Use Button.
<button
on:click=move |_| data_state.update(|s| *s = match s { SidenavState::Expanded => SidenavState::Collapsed, SidenavState::Collapsed => SidenavState::Expanded })
onclick=ONCLICK_TRIGGER
data-name="SidenavTrigger"
class=tw_merge!("inline-flex gap-2 justify-center items-center text-sm font-medium whitespace-nowrap rounded-md transition-all outline-none size-7 hover:bg-accent hover:text-accent-foreground focus-visible:ring-2", class)
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]"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
{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

@@ -192,7 +192,7 @@ pub async fn subscribe_to_push_notifications() {
let key_array = js_sys::Uint8Array::from(&decoded_key[..]);
// 3. Prepare Options
let mut options = web_sys::PushSubscriptionOptionsInit::new();
let options = web_sys::PushSubscriptionOptionsInit::new();
options.set_user_visible_only(true);
options.set_application_server_key(&key_array.into());