Compare commits

..

14 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
spinline
3ce980239c fix: ensure context menu closes after triggering start or stop actions
All checks were successful
Build MIPS Binary / build (push) Successful in 1m51s
2026-02-14 01:25:47 +03:00
spinline
d00fc41010 style: update CSS configuration and theme variables
All checks were successful
Build MIPS Binary / build (push) Successful in 1m55s
2026-02-13 20:11:02 +03:00
spinline
0636020a86 chore: re-add Leptos metadata to Cargo.toml for ui-cli compatibility
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-13 20:03:01 +03:00
23 changed files with 579 additions and 1231 deletions

View File

@@ -1,4 +1,3 @@
mod components;
mod diff; mod diff;
mod handlers; mod handlers;
#[cfg(feature = "push-notifications")] #[cfg(feature = "push-notifications")]

View File

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

View File

@@ -1,6 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
@@ -44,6 +45,7 @@
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
@@ -80,10 +82,10 @@
button:not(:disabled), button:not(:disabled),
[role="button"]:not(:disabled) { [role="button"]:not(:disabled) {
@apply cursor-pointer; cursor: pointer;
} }
dialog { dialog {
@apply m-auto; margin: auto;
} }
} }

View File

@@ -1,34 +1,24 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use crate::components::ui::card::{Card, CardHeader, CardContent}; use crate::components::ui::card::{Card, CardHeader, CardContent};
use crate::components::ui::auto_form::{AutoForm, AutoFormField}; use crate::components::ui::input::{Input, InputType};
use crate::components::ui::button::Button;
#[component] #[component]
pub fn Login() -> impl IntoView { pub fn Login() -> impl IntoView {
let username = RwSignal::new(String::new());
let password = RwSignal::new(String::new());
let error = signal(Option::<String>::None); let error = signal(Option::<String>::None);
let loading = signal(false); let loading = signal(false);
let fields = vec![ let handle_login = move |ev: web_sys::SubmitEvent| {
AutoFormField::Text { ev.prevent_default();
name: "username".to_string(),
label: "Kullanıcı Adı".to_string(),
placeholder: Some("Kullanıcı adınız".to_string()),
required: true,
},
AutoFormField::Password {
name: "password".to_string(),
label: "Şifre".to_string(),
placeholder: Some("******".to_string()),
required: true,
},
];
let on_submit = move |data: std::collections::HashMap<String, String>| {
loading.1.set(true); loading.1.set(true);
error.1.set(None); error.1.set(None);
let user = data.get("username").cloned().unwrap_or_default(); let user = username.get();
let pass = data.get("password").cloned().unwrap_or_default(); let pass = password.get();
spawn_local(async move { spawn_local(async move {
match shared::server_fns::auth::login(user, pass).await { match shared::server_fns::auth::login(user, pass).await {
@@ -59,20 +49,47 @@ pub fn Login() -> impl IntoView {
</CardHeader> </CardHeader>
<CardContent class="pt-4"> <CardContent class="pt-4">
<AutoForm <form on:submit=handle_login class="space-y-4">
fields=fields <div class="space-y-2">
submit_label="Giriş Yap" <label class="text-sm font-medium leading-none">"Kullanıcı Adı"</label>
on_submit=on_submit <Input
loading=loading.0.into() r#type=InputType::Text
/> placeholder="Kullanıcı adınız"
bind_value=username
<Show when=move || error.0.get().is_some()> disabled=loading.0.get()
<div class="mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"> />
{move || error.0.get().unwrap_or_default()}
</div> </div>
</Show> <div class="space-y-2">
<label class="text-sm font-medium leading-none">"Şifre"</label>
<Input
r#type=InputType::Password
placeholder="******"
bind_value=password
disabled=loading.0.get()
/>
</div>
<Show when=move || error.0.get().is_some()>
<div class="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
{move || error.0.get().unwrap_or_default()}
</div>
</Show>
<div class="pt-2">
<Button
class="w-full"
attr:r#type="submit"
attr:disabled=move || loading.0.get()
>
<Show when=move || loading.0.get() fallback=|| view! { "Giriş Yap" }.into_any()>
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
"Giriş Yapılıyor..."
</Show>
</Button>
</div>
</form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
} }
} }

View File

@@ -1,38 +1,23 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use crate::components::ui::card::{Card, CardHeader, CardContent}; use crate::components::ui::card::{Card, CardHeader, CardContent};
use crate::components::ui::auto_form::{AutoForm, AutoFormField}; use crate::components::ui::input::{Input, InputType};
use crate::components::ui::button::Button;
#[component] #[component]
pub fn Setup() -> impl IntoView { pub fn Setup() -> impl IntoView {
let username = RwSignal::new(String::new());
let password = RwSignal::new(String::new());
let confirm_password = RwSignal::new(String::new());
let error = signal(Option::<String>::None); let error = signal(Option::<String>::None);
let loading = signal(false); let loading = signal(false);
let fields = vec![ let handle_setup = move |ev: web_sys::SubmitEvent| {
AutoFormField::Text { ev.prevent_default();
name: "username".to_string(),
label: "Yönetici Kullanıcı Adı".to_string(), let pass = password.get();
placeholder: Some("admin".to_string()), let confirm = confirm_password.get();
required: true,
},
AutoFormField::Password {
name: "password".to_string(),
label: "Şifre".to_string(),
placeholder: Some("******".to_string()),
required: true,
},
AutoFormField::Password {
name: "confirm_password".to_string(),
label: "Şifre Onay".to_string(),
placeholder: Some("******".to_string()),
required: true,
},
];
let on_submit = move |data: std::collections::HashMap<String, String>| {
let user = data.get("username").cloned().unwrap_or_default();
let pass = data.get("password").cloned().unwrap_or_default();
let confirm = data.get("confirm_password").cloned().unwrap_or_default();
if pass != confirm { if pass != confirm {
error.1.set(Some("Şifreler eşleşmiyor".to_string())); error.1.set(Some("Şifreler eşleşmiyor".to_string()));
@@ -47,6 +32,8 @@ pub fn Setup() -> impl IntoView {
loading.1.set(true); loading.1.set(true);
error.1.set(None); error.1.set(None);
let user = username.get();
spawn_local(async move { spawn_local(async move {
match shared::server_fns::auth::setup(user, pass).await { match shared::server_fns::auth::setup(user, pass).await {
Ok(_) => { Ok(_) => {
@@ -77,18 +64,54 @@ pub fn Setup() -> impl IntoView {
</CardHeader> </CardHeader>
<CardContent class="pt-4"> <CardContent class="pt-4">
<AutoForm <form on:submit=handle_setup class="space-y-4">
fields=fields <div class="space-y-2">
submit_label="Kurulumu Tamamla" <label class="text-sm font-medium leading-none">"Yönetici Kullanıcı Adı"</label>
on_submit=on_submit <Input
loading=loading.0.into() r#type=InputType::Text
/> placeholder="admin"
bind_value=username
<Show when=move || error.0.get().is_some() fallback=|| ()> disabled=loading.0.get()
<div class="mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"> />
<span>{move || error.0.get().unwrap_or_default()}</span>
</div> </div>
</Show> <div class="space-y-2">
<label class="text-sm font-medium leading-none">"Şifre"</label>
<Input
r#type=InputType::Password
placeholder="******"
bind_value=password
disabled=loading.0.get()
/>
</div>
<div class="space-y-2">
<label class="text-sm font-medium leading-none">"Şifre Onay"</label>
<Input
r#type=InputType::Password
placeholder="******"
bind_value=confirm_password
disabled=loading.0.get()
/>
</div>
<Show when=move || error.0.get().is_some() fallback=|| ()>
<div class="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
<span>{move || error.0.get().unwrap_or_default()}</span>
</div>
</Show>
<div class="pt-2">
<Button
class="w-full"
attr:r#type="submit"
attr:disabled=move || loading.0.get()
>
<Show when=move || loading.0.get() fallback=|| view! { "Kurulumu Tamamla" }.into_any()>
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
"Kuruluyor..."
</Show>
</Button>
</div>
</form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -24,10 +24,16 @@ pub fn TorrentContextMenu(
{children()} {children()}
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent class="w-56 p-1.5"> <ContextMenuContent class="w-56 p-1.5">
<ContextMenuItem on:click={let h = hash_c1; move |_| on_action_stored.get_value().run(("start".to_string(), h.clone()))}> <ContextMenuItem on:click={let h = hash_c1; move |_| {
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 |_| on_action_stored.get_value().run(("stop".to_string(), h.clone()))}> <ContextMenuItem on:click={let h = hash_c2; move |_| {
on_action_stored.get_value().run(("stop".to_string(), h.clone()));
crate::components::ui::context_menu::close_context_menu();
}}>
"Durdur" "Durdur"
</ContextMenuItem> </ContextMenuItem>

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

@@ -3,20 +3,53 @@ 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 /> <Toolbar on_toggle_sidebar=Callback::new(move |_| set_collapsed.update(|c| *c = !*c)) />
// 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"> <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 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,26 +150,28 @@ 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"> <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-[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>
<Switch <div class="group-data-[state=Collapsed]:hidden">
checked=Signal::from(store.push_enabled) <Switch
on_checked_change=Callback::new(on_push_toggle) checked=Signal::from(store.push_enabled)
/> 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"> <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"> <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"> <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="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"> <div class="flex items-center gap-1 group-data-[state=Collapsed]:hidden">
<ThemeToggle /> <ThemeToggle />
<Button <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"> <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">{label}</span> <span class="flex-1 truncate group-data-[state=Collapsed]:hidden">{label}</span>
<span class="text-[10px] font-mono opacity-50">{count}</span> <span class="text-[10px] font-mono opacity-50 group-data-[state=Collapsed]:hidden">{count}</span>
</SidenavMenuButton> </SidenavMenuButton>
</SidenavMenuItem> </SidenavMenuItem>
} }

View File

@@ -1,24 +1,38 @@
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::{ButtonVariant, ButtonSize}; use crate::components::ui::button::{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() -> impl IntoView { pub fn Toolbar(
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
// --- MOBILE SHEET (SIDEBAR) --- <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"> <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">"Menüyü Aç"</span> <span class="hidden">"Open Menu"</span>
</SheetTrigger> </SheetTrigger>
<SheetContent <SheetContent
direction=SheetDirection::Left direction=SheetDirection::Left

View File

@@ -1,93 +0,0 @@
use leptos::prelude::*;
use tailwind_fuse::tw_merge;
use crate::components::ui::button::Button;
use crate::components::ui::input::{Input, InputType};
#[derive(Clone, Debug)]
pub enum AutoFormField {
Text {
name: String,
label: String,
placeholder: Option<String>,
required: bool,
},
Password {
name: String,
label: String,
placeholder: Option<String>,
required: bool,
},
}
#[component]
pub fn AutoForm(
#[prop(into)] fields: Vec<AutoFormField>,
#[prop(into)] submit_label: String,
#[prop(into)] on_submit: Callback<std::collections::HashMap<String, String>>,
#[prop(optional)] loading: Signal<bool>,
#[prop(optional, into)] class: String,
) -> impl IntoView {
let field_values = fields.iter().map(|f| {
let name = match f {
AutoFormField::Text { name, .. } => name,
AutoFormField::Password { name, .. } => name,
};
(name.clone(), RwSignal::new(String::new()))
}).collect::<std::collections::HashMap<String, RwSignal<String>>>();
let handle_submit = {
let field_values = field_values.clone();
move |ev: web_sys::SubmitEvent| {
ev.prevent_default();
let mut data = std::collections::HashMap::new();
for (name, signal) in &field_values {
data.insert(name.clone(), signal.get());
}
on_submit.run(data);
}
};
view! {
<form on:submit=handle_submit class=tw_merge!("space-y-4", class)>
{fields.into_iter().map(|field| {
let (name, label, placeholder, r#type, required) = match field {
AutoFormField::Text { name, label, placeholder, required } => (name, label, placeholder, InputType::Text, required),
AutoFormField::Password { name, label, placeholder, required } => (name, label, placeholder, InputType::Password, required),
};
let signal = field_values.get(&name).cloned().unwrap();
view! {
<div class="space-y-2">
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{label}
</label>
<Input
r#type=r#type
placeholder=placeholder.unwrap_or_default()
bind_value=signal
required=required
disabled=loading.get()
/>
</div>
}
}).collect_view()}
<div class="pt-2">
<Button
class="w-full"
attr:r#type="submit"
attr:disabled=move || loading.get()
>
<Show when=move || loading.get() fallback=move || {
let label = submit_label.clone();
view! { <span>{label}</span> }.into_any()
}>
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
"İşleniyor..."
</Show>
</Button>
</div>
</form>
}
}

View File

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

View File

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

View File

@@ -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

@@ -1,6 +1,5 @@
pub mod accordion; pub mod accordion;
pub mod alert_dialog; pub mod alert_dialog;
pub mod auto_form;
pub mod badge; pub mod badge;
pub mod button; pub mod button;
pub mod button_action; pub mod button_action;

View File

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

View File

@@ -1,26 +0,0 @@
use leptos::prelude::*;
use tailwind_fuse::tw_merge;
#[component]
pub fn Progress(
#[prop(into)] value: Signal<f64>,
#[prop(optional, into)] class: String,
) -> impl IntoView {
let progress_style = move || format!("transform: translateX(-{}%);", 100.0 - value.get().clamp(0.0, 100.0));
view! {
<div
data-name="Progress"
class=tw_merge!(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
class
)
>
<div
data-name="ProgressIndicator"
class="h-full w-full flex-1 bg-primary transition-all duration-500 ease-in-out"
style=progress_style
/>
</div>
}
}

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

@@ -145,7 +145,8 @@ pub fn Sidenav(
data-name="Sidenav" data-name="Sidenav"
data-sidenav=data_state.to_string() data-sidenav=data_state.to_string()
data-side=data_side.to_string() data-side=data_side.to_string()
class="hidden md:block group peer text-sidenav-foreground data-[state=Collapsed]:hidden" 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 // * SidenavGap: This is what handles the sidenav gap on desktop
<div <div
@@ -155,9 +156,9 @@ pub fn Sidenav(
"group-data-[collapsible=Offcanvas]:w-0", "group-data-[collapsible=Offcanvas]:w-0",
"group-data-[side=Right]:rotate-180", "group-data-[side=Right]:rotate-180",
match variant { match variant {
SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:w-(--sidenav-width-icon)", SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-(--sidenav-width-icon)",
SidenavVariant::Floating | SidenavVariant::Inset => SidenavVariant::Floating | SidenavVariant::Inset =>
"group-data-[collapsible=Icon]:w-[calc(var(--sidenav-width-icon)+(--spacing(4)))]", "group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4)))]",
} }
) )
/> />
@@ -171,9 +172,9 @@ pub fn Sidenav(
SidenavSide::Right => "right-0 group-data-[collapsible=Offcanvas]:right-[calc(var(--sidenav-width)*-1)]" SidenavSide::Right => "right-0 group-data-[collapsible=Offcanvas]:right-[calc(var(--sidenav-width)*-1)]"
}, },
match variant { match variant {
SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:w-(--sidenav-width-icon) group-data-[side=Left]:border-r group-data-[side=Right]:border-l", 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 => SidenavVariant::Floating | SidenavVariant::Inset =>
"p-2 group-data-[collapsible=Icon]:w-[calc(var(--sidenav-width-icon)+(--spacing(4))+2px)]", "p-2 group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4))+2px)]",
}, },
) )
> >

View File

@@ -192,7 +192,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 mut options = web_sys::PushSubscriptionOptionsInit::new(); let 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());

1369
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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