Compare commits

..

1 Commits

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

3
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -3,53 +3,20 @@ use crate::components::layout::sidebar::Sidebar;
use crate::components::layout::toolbar::Toolbar;
use crate::components::layout::footer::Footer;
use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset};
use wasm_bindgen::JsCast;
#[component]
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:3rem;">
// Masaüstü 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 }
>
<Sidenav>
<Sidebar />
</Sidenav>
// İçerik Alanı
<SidenavInset class="flex flex-col h-screen overflow-hidden">
// Toolbar (Üst Bar)
<Toolbar on_toggle_sidebar=Callback::new(move |_| set_collapsed.update(|c| *c = !*c)) />
<Toolbar />
// Ana İçerik
<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" />
</svg>
</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 text-[10px] text-muted-foreground opacity-70">"v3.0.0"</span>
</div>
@@ -150,28 +150,26 @@ 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 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-[9px] text-muted-foreground">"Web Push"</span>
</div>
<div class="group-data-[state=Collapsed]:hidden">
<Switch
checked=Signal::from(store.push_enabled)
on_checked_change=Callback::new(on_push_toggle)
/>
</div>
<Switch
checked=Signal::from(store.push_enabled)
on_checked_change=Callback::new(on_push_toggle)
/>
</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">
{first_letter}
</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="text-[9px] text-muted-foreground truncate opacity-70">"Yönetici"</div>
</div>
<div class="flex items-center gap-1 group-data-[state=Collapsed]:hidden">
<div class="flex items-center gap-1">
<ThemeToggle />
<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">
<path stroke-linecap="round" stroke-linejoin="round" d=icon.clone() />
</svg>
<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>
<span class="flex-1 truncate">{label}</span>
<span class="text-[10px] font-mono opacity-50">{count}</span>
</SidenavMenuButton>
</SidenavMenuItem>
}

View File

@@ -1,38 +1,24 @@
use leptos::prelude::*;
use icons::{PanelLeft, Plus};
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::dialog::{Dialog, DialogContent, DialogTrigger};
use crate::components::layout::sidebar::Sidebar;
#[component]
pub fn Toolbar(
on_toggle_sidebar: Callback<()>,
) -> impl IntoView {
pub fn Toolbar() -> impl IntoView {
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)
// --- MOBILE SHEET (SIDEBAR) ---
<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>
<span class="hidden">"Menüyü Aç"</span>
</SheetTrigger>
<SheetContent
direction=SheetDirection::Left

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// * Reuse @table.rs
pub use crate::components::ui::table::{
Table as DataTable, TableBody as DataTableBody, TableCell as DataTableCell,
TableHead as DataTableHead, TableHeader as DataTableHeader,
Table as DataTable, TableBody as DataTableBody, TableCaption as DataTableCaption, TableCell as DataTableCell,
TableFooter as DataTableFooter, 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

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

View File

@@ -9,7 +9,7 @@ use crate::components::hooks::use_can_scroll_vertical::use_can_scroll_vertical;
use crate::components::hooks::use_random::use_random_id_for;
// * Reuse @select.rs
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)]

View File

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

View File

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

View File

@@ -1,233 +1,79 @@
use leptos::prelude::*;
use leptos_router::hooks::use_location;
use leptos_ui::{clx, variants, void};
use tw_merge::tw_merge;
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"}
clx! {SidenavMenuSub, ul, "border-sidenav-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=Icon]:hidden"}
clx! {SidenavMenuItem, li, "relative group/menu-item"}
clx! {SidenavContent, div, "scrollbar__on_hover", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=Icon]:overflow-hidden"}
clx! {SidenavGroup, div, "flex relative flex-col p-2 w-full min-w-0"}
clx! {SidenavGroupContent, div, "w-full text-sm"}
clx! {SidenavGroupLabel, div, "text-sidenav-foreground/70 ring-sidenav-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=Icon]:-mt-8 group-data-[collapsible=Icon]:opacity-0"}
clx! {SidenavFooter, footer, "flex flex-col gap-2 p-2"}
// Button "More"
clx! {DropdownMenuTriggerEllipsis, button, "text-sidenav-foreground ring-sidenav-ring hover:bg-sidenav-accent hover:text-sidenav-accent-foreground peer-hover/menu-button:text-sidenav-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 group-data-[collapsible=Icon]:hidden peer-data-[active=true]/menu-button:text-sidenav-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0"}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
#[allow(dead_code)]
pub enum SidenavState { #[default] Expanded, Collapsed }
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", // 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"
}
#[derive(Clone)]
pub struct SidenavContext {
pub state: RwSignal<SidenavState>,
}
pub use components::*;
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
#[component]
pub fn SidenavWrapper(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let state = RwSignal::new(SidenavState::Expanded);
provide_context(SidenavContext { state });
let class = tw_merge!("flex min-h-screen w-full bg-background", class);
view! { <div class=class>{children()}</div> }
}
#[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))
pub fn Sidenav(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let ctx = expect_context::<SidenavContext>();
let class_signal = move || {
let width_class = match ctx.state.get() {
SidenavState::Expanded => "w-[var(--sidenav-width)]",
SidenavState::Collapsed => "w-[var(--sidenav-width-icon)]",
};
tw_merge!(
"hidden md:flex flex-col border-r bg-card transition-all duration-300",
width_class,
class.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]
Sidenav,
Floating,
Inset,
}
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display)]
pub enum SidenavSide {
#[default]
Left,
Right,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, strum::Display)]
pub enum SidenavCollapsible {
#[default]
Offcanvas,
None,
Icon,
view! { <aside class=class_signal>{children()}</aside> }
}
#[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,
pub fn SidenavInset(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("flex flex-col flex-1 min-w-0", class);
view! { <main class=class>{children()}</main> }
}
#[component] pub fn SidenavHeader(children: Children) -> impl IntoView { view! { <div class="flex flex-col">{children()}</div> } }
#[component] pub fn SidenavContent(children: Children) -> impl IntoView { view! { <div class="flex-1 overflow-auto">{children()}</div> } }
#[component] pub fn SidenavFooter(children: Children) -> impl IntoView { view! { <div class="mt-auto">{children()}</div> } }
#[component] pub fn SidenavGroup(children: Children) -> impl IntoView { view! { <div class="px-2 py-2">{children()}</div> } }
#[component] pub fn SidenavGroupLabel(children: Children) -> impl IntoView { view! { <div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">{children()}</div> } }
#[component] pub fn SidenavGroupContent(children: Children) -> impl IntoView { view! { <div class="space-y-1">{children()}</div> } }
#[component] pub fn SidenavMenu(children: Children) -> impl IntoView { view! { <nav class="grid gap-1">{children()}</nav> } }
#[component] pub fn SidenavMenuItem(children: Children) -> impl IntoView { view! { <div>{children()}</div> } }
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum SidenavMenuButtonVariant { #[default] Default, Outline }
#[component]
pub fn SidenavMenuButton(
children: Children,
#[prop(into, optional)] variant: Signal<SidenavMenuButtonVariant>,
#[prop(into, optional)] class: Signal<String>,
) -> 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()
}}
}
let class_signal = move || {
let variant_class = if variant.get() == SidenavMenuButtonVariant::Outline {
"border border-input bg-background shadow-xs"
} else {
""
};
tw_merge!(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
variant_class,
class.get()
)
};
view! { <button class=class_signal>{children()}</button> }
}
/* ========================================================== */
/* ✨ FUNCTIONS ✨ */
/* ========================================================== */
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display)]
pub enum SidenavState {
#[default]
Expanded,
Collapsed,
#[component] pub fn SidenavLink(children: Children, #[prop(into)] href: String) -> impl IntoView {
view! { <a href=href class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent">{children()}</a> }
}
const ONCLICK_TRIGGER: &str = "document.querySelector('[data-name=\"Sidenav\"]').setAttribute('data-state', document.querySelector('[data-name=\"Sidenav\"]').getAttribute('data-state') === 'Collapsed' ? 'Expanded' : 'Collapsed')";
#[component]
pub fn SidenavTrigger(children: Children) -> impl IntoView {
view! {
// TODO. Use Button.
<button
onclick=ONCLICK_TRIGGER
data-name="SidenavTrigger"
class="inline-flex gap-2 justify-center items-center -ml-1 text-sm font-medium whitespace-nowrap rounded-md transition-all outline-none disabled:opacity-50 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 aria-invalid:ring-destructive/20 aria-invalid:border-destructive size-7 dark:aria-invalid:ring-destructive/40 dark:hover:bg-accent/50 hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
>
{children()}
</button>
}
}
#[component]
fn SidenavToggleRail() -> impl IntoView {
view! {
<button
data-name="SidenavToggleRail"
aria-label="Toggle Sidenav"
tabindex="-1"
onclick=ONCLICK_TRIGGER
class="hidden absolute inset-y-0 z-20 w-4 transition-all ease-linear -translate-x-1/2 sm:flex group-data-[side=Left]:-right-4 group-data-[side=Right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] in-data-[side=Left]:cursor-w-resize in-data-[side=Right]:cursor-e-resize [[data-side=Left][data-state=Collapsed]_&]:cursor-e-resize [[data-side=Right][data-state=Collapsed]_&]:cursor-w-resize group-data-[collapsible=Offcanvas]:translate-x-0 group-data-[collapsible=Offcanvas]:after:left-full [[data-side=Left][data-collapsible=Offcanvas]_&]:-right-2 [[data-side=Right][data-collapsible=Offcanvas]_&]:-left-0eft-2 hover:after:bg-sidenav-border hover:group-data-[collapsible=Offcanvas]:bg-sidenav"
/>
}
}

View File

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