Compare commits

..

1 Commits

Author SHA1 Message Date
spinline
b2f856f80f feat: implement AutoForm component and refactor Login/Setup screens
All checks were successful
Build MIPS Binary / build (push) Successful in 1m56s
2026-02-13 18:36:58 +03:00
23 changed files with 252 additions and 644 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

@@ -1,24 +1,34 @@
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::input::{Input, InputType}; use crate::components::ui::auto_form::{AutoForm, AutoFormField};
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 handle_login = move |ev: web_sys::SubmitEvent| { let fields = vec![
ev.prevent_default(); AutoFormField::Text {
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 = username.get(); let user = data.get("username").cloned().unwrap_or_default();
let pass = password.get(); let pass = data.get("password").cloned().unwrap_or_default();
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 {
@@ -49,47 +59,20 @@ pub fn Login() -> impl IntoView {
</CardHeader> </CardHeader>
<CardContent class="pt-4"> <CardContent class="pt-4">
<form on:submit=handle_login class="space-y-4"> <AutoForm
<div class="space-y-2"> fields=fields
<label class="text-sm font-medium leading-none">"Kullanıcı Adı"</label> submit_label="Giriş Yap"
<Input on_submit=on_submit
r#type=InputType::Text loading=loading.0.into()
placeholder="Kullanıcı adınız" />
bind_value=username
disabled=loading.0.get()
/>
</div>
<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()> <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"> <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()} {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> </div>
</form> </Show>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
} }
} }

View File

@@ -1,23 +1,38 @@
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::input::{Input, InputType}; use crate::components::ui::auto_form::{AutoForm, AutoFormField};
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 handle_setup = move |ev: web_sys::SubmitEvent| { let fields = vec![
ev.prevent_default(); AutoFormField::Text {
name: "username".to_string(),
let pass = password.get(); label: "Yönetici Kullanıcı Adı".to_string(),
let confirm = confirm_password.get(); placeholder: Some("admin".to_string()),
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()));
@@ -32,8 +47,6 @@ 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(_) => {
@@ -64,54 +77,18 @@ pub fn Setup() -> impl IntoView {
</CardHeader> </CardHeader>
<CardContent class="pt-4"> <CardContent class="pt-4">
<form on:submit=handle_setup class="space-y-4"> <AutoForm
<div class="space-y-2"> fields=fields
<label class="text-sm font-medium leading-none">"Yönetici Kullanıcı Adı"</label> submit_label="Kurulumu Tamamla"
<Input on_submit=on_submit
r#type=InputType::Text loading=loading.0.into()
placeholder="admin" />
bind_value=username
disabled=loading.0.get()
/>
</div>
<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=|| ()> <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"> <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> <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> </div>
</form> </Show>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -24,16 +24,10 @@ 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 |_| { <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>

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

@@ -0,0 +1,93 @@
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, 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

@@ -1,5 +1,6 @@
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, SelectGroup as MultiSelectGroup, SelectItem as MultiSelectItem, SelectLabel as MultiSelectLabel,
}; };
#[derive(Clone, Copy, PartialEq, Eq, Default)] #[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"} 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,8 +145,7 @@ 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()
data-collapsible=data_collapsible.to_string() class="hidden md:block group peer text-sidenav-foreground data-[state=Collapsed]:hidden"
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
@@ -156,9 +155,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]:group-data-[state=Collapsed]:w-(--sidenav-width-icon)", SidenavVariant::Sidenav => "group-data-[collapsible=Icon]:w-(--sidenav-width-icon)",
SidenavVariant::Floating | SidenavVariant::Inset => SidenavVariant::Floating | SidenavVariant::Inset =>
"group-data-[collapsible=Icon]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4)))]", "group-data-[collapsible=Icon]:w-[calc(var(--sidenav-width-icon)+(--spacing(4)))]",
} }
) )
/> />
@@ -172,9 +171,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]:group-data-[state=Collapsed]:w-(--sidenav-width-icon) group-data-[side=Left]:border-r group-data-[side=Right]:border-l", SidenavVariant::Sidenav => "group-data-[collapsible=Icon]: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]:group-data-[state=Collapsed]:w-[calc(var(--sidenav-width-icon)+(--spacing(4))+2px)]", "p-2 group-data-[collapsible=Icon]: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 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"