From 63128f8501e16e5f63573d63ff4372909e979872 Mon Sep 17 00:00:00 2001 From: spinline Date: Sat, 31 Jan 2026 13:47:48 +0300 Subject: [PATCH] feat: integrate shadcn/ui, add Button component, and refactor App UI --- Cargo.lock | 10 + frontend/Cargo.toml | 1 + frontend/input.css | 148 ++++- frontend/public/tailwind.css | 582 +++++++++---------- frontend/src/app.rs | 797 +++++++++++++-------------- frontend/src/components/mod.rs | 4 +- frontend/src/components/modal.rs | 19 +- frontend/src/components/ui/button.rs | 62 +++ frontend/src/components/ui/mod.rs | 1 + frontend/src/lib.rs | 1 + frontend/src/utils/mod.rs | 5 + 11 files changed, 928 insertions(+), 702 deletions(-) create mode 100644 frontend/src/components/ui/button.rs create mode 100644 frontend/src/components/ui/mod.rs create mode 100644 frontend/src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d4b62fc..b57c6f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,7 @@ dependencies = [ "serde", "serde_json", "shared", + "tailwind_fuse", "uuid", "wasm-bindgen", "web-sys", @@ -2225,6 +2226,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tailwind_fuse" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca71fb01735fbc6fa13e9390d7a3037dde97053c0b65c0c72c0159cd009d26b" +dependencies = [ + "nom", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 259343a..c6f3d90 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -20,3 +20,4 @@ futures = "0.3" chrono = { version = "0.4", features = ["serde"] } web-sys = { version = "0.3", features = ["Window", "Storage"] } shared = { path = "../shared" } +tailwind_fuse = "0.3.2" diff --git a/frontend/input.css b/frontend/input.css index 5d64175..84a0e6a 100644 --- a/frontend/input.css +++ b/frontend/input.css @@ -16,8 +16,150 @@ } } +@layer base { + + :root { + --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 10% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --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%; + } + + .amoled { + --background: 0 0% 0%; + --foreground: 0 0% 98%; + --card: 0 0% 0%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 0%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 0%; + --secondary: 0 0% 9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 9%; + --muted-foreground: 0 0% 60%; + --accent: 0 0% 9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 12%; + --input: 0 0% 12%; + --ring: 0 0% 83.9%; + } + + * { + @apply border-border; + } +} + @theme { - --color-gray-900: #111827; - --color-gray-800: #1f2937; - --color-gray-700: #374151; + --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-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0 + } + + to { + height: var(--radix-accordion-content-height) + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height) + } + + to { + height: 0 + } + } } \ No newline at end of file diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css index e0acdd9..2e00139 100644 --- a/frontend/public/tailwind.css +++ b/frontend/public/tailwind.css @@ -14,21 +14,15 @@ --color-yellow-500: oklch(79.5% 0.184 86.047); --color-green-500: oklch(72.3% 0.219 149.579); --color-blue-500: oklch(62.3% 0.214 259.815); - --color-blue-600: oklch(54.6% 0.245 262.881); - --color-indigo-600: oklch(51.1% 0.262 276.966); --color-purple-500: oklch(62.7% 0.265 303.9); --color-purple-600: oklch(55.8% 0.288 302.321); --color-pink-500: oklch(65.6% 0.241 354.308); - --color-gray-50: oklch(98.5% 0.002 247.839); --color-gray-100: oklch(96.7% 0.003 264.542); - --color-gray-200: oklch(92.8% 0.006 264.531); --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-400: oklch(70.7% 0.022 261.325); --color-gray-500: oklch(55.1% 0.027 264.364); - --color-gray-600: oklch(44.6% 0.03 256.802); - --color-gray-700: #374151; - --color-gray-800: #1f2937; - --color-gray-900: #111827; + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -48,19 +42,38 @@ --text-6xl: 3.75rem; --text-6xl--line-height: 1; --font-weight-medium: 500; + --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-tight: -0.025em; --tracking-wider: 0.05em; --tracking-widest: 0.1em; + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); --radius-xl: 0.75rem; --radius-2xl: 1rem; --blur-sm: 8px; - --blur-md: 12px; --blur-xl: 24px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-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)); } } @layer base { @@ -254,8 +267,8 @@ .left-0 { left: calc(var(--spacing) * 0); } - .left-4 { - left: calc(var(--spacing) * 4); + .left-3 { + left: calc(var(--spacing) * 3); } .z-10 { z-index: 10; @@ -311,9 +324,6 @@ .mb-6 { margin-bottom: calc(var(--spacing) * 6); } - .mb-8 { - margin-bottom: calc(var(--spacing) * 8); - } .mb-10 { margin-bottom: calc(var(--spacing) * 10); } @@ -341,6 +351,9 @@ .hidden { display: none; } + .inline-flex { + display: inline-flex; + } .h-1\.5 { height: calc(var(--spacing) * 1.5); } @@ -356,9 +369,15 @@ .h-6 { height: calc(var(--spacing) * 6); } + .h-9 { + height: calc(var(--spacing) * 9); + } .h-10 { height: calc(var(--spacing) * 10); } + .h-11 { + height: calc(var(--spacing) * 11); + } .h-12 { height: calc(var(--spacing) * 12); } @@ -468,6 +487,9 @@ .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } .gap-1\.5 { gap: calc(var(--spacing) * 1.5); } @@ -510,6 +532,11 @@ border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); } } + .divide-border { + :where(& > :not(:last-child)) { + border-color: var(--color-border); + } + } .truncate { overflow: hidden; text-overflow: ellipsis; @@ -530,6 +557,9 @@ .rounded-full { border-radius: calc(infinity * 1px); } + .rounded-md { + border-radius: var(--radius-md); + } .rounded-xl { border-radius: var(--radius-xl); } @@ -562,14 +592,14 @@ border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); } } - .border-blue-500\/30 { - border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-blue-500) 30%, transparent); - } + .border-border { + border-color: var(--color-border); } - .border-gray-200 { - border-color: var(--color-gray-200); + .border-destructive\/20 { + border-color: color-mix(in srgb, hsl(var(--destructive)) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-destructive) 20%, transparent); + } } .border-gray-300 { border-color: var(--color-gray-300); @@ -586,21 +616,18 @@ border-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); } } - .border-red-500\/20 { - border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); + .border-input { + border-color: var(--color-input); + } + .border-primary\/20 { + border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-red-500) 20%, transparent); + border-color: color-mix(in oklab, var(--color-primary) 20%, transparent); } } .border-transparent { border-color: transparent; } - .border-white\/5 { - border-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } .border-white\/10 { border-color: color-mix(in srgb, #fff 10%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -616,18 +643,18 @@ .bg-\[\#0a0a0c\] { background-color: #0a0a0c; } - .bg-\[\#16161c\] { - background-color: #16161c; - } - .bg-\[\#111116\] { - background-color: #111116; - } - .bg-\[\#111116\]\/80 { - background-color: color-mix(in oklab, #111116 80%, transparent); - } .bg-\[\#111116\]\/95 { background-color: color-mix(in oklab, #111116 95%, transparent); } + .bg-background { + background-color: var(--color-background); + } + .bg-background\/80 { + background-color: color-mix(in srgb, hsl(var(--background)) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-background) 80%, transparent); + } + } .bg-black { background-color: var(--color-black); } @@ -637,24 +664,6 @@ background-color: color-mix(in oklab, var(--color-black) 5%, transparent); } } - .bg-black\/30 { - background-color: color-mix(in srgb, #000 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 30%, transparent); - } - } - .bg-black\/60 { - background-color: color-mix(in srgb, #000 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 60%, transparent); - } - } - .bg-black\/80 { - background-color: color-mix(in srgb, #000 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 80%, transparent); - } - } .bg-blue-500 { background-color: var(--color-blue-500); } @@ -664,14 +673,26 @@ background-color: color-mix(in oklab, var(--color-blue-500) 10%, transparent); } } - .bg-blue-600\/20 { - background-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 20%, transparent); + .bg-card { + background-color: var(--color-card); + } + .bg-card\/50 { + background-color: color-mix(in srgb, hsl(var(--card)) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-600) 20%, transparent); + background-color: color-mix(in oklab, var(--color-card) 50%, transparent); } } - .bg-gray-50 { - background-color: var(--color-gray-50); + .bg-destructive { + background-color: var(--color-destructive); + } + .bg-destructive\/10 { + background-color: color-mix(in srgb, hsl(var(--destructive)) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-destructive) 10%, transparent); + } + } + .bg-foreground { + background-color: var(--color-foreground); } .bg-gray-100 { background-color: var(--color-gray-100); @@ -679,18 +700,6 @@ .bg-gray-400 { background-color: var(--color-gray-400); } - .bg-gray-500\/10 { - background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-gray-500) 10%, transparent); - } - } - .bg-gray-500\/20 { - background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-gray-500) 20%, transparent); - } - } .bg-green-500 { background-color: var(--color-green-500); } @@ -700,22 +709,37 @@ background-color: color-mix(in oklab, var(--color-green-500) 10%, transparent); } } + .bg-input { + background-color: var(--color-input); + } + .bg-muted { + background-color: var(--color-muted); + } + .bg-muted\/50 { + background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-muted) 50%, transparent); + } + } + .bg-primary { + background-color: var(--color-primary); + } + .bg-primary\/10 { + background-color: color-mix(in srgb, hsl(var(--primary)) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 10%, transparent); + } + } .bg-red-500 { background-color: var(--color-red-500); } - .bg-red-500\/10 { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent); - } + .bg-secondary { + background-color: var(--color-secondary); } - .bg-white { - background-color: var(--color-white); - } - .bg-white\/5 { - background-color: color-mix(in srgb, #fff 5%, transparent); + .bg-secondary\/50 { + background-color: color-mix(in srgb, hsl(var(--secondary)) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); + background-color: color-mix(in oklab, var(--color-secondary) 50%, transparent); } } .bg-white\/10 { @@ -724,12 +748,6 @@ background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } - .bg-white\/80 { - background-color: color-mix(in srgb, #fff 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 80%, transparent); - } - } .bg-yellow-500 { background-color: var(--color-yellow-500); } @@ -751,19 +769,11 @@ --tw-gradient-from: var(--color-blue-500); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } - .from-blue-600 { - --tw-gradient-from: var(--color-blue-600); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .via-purple-500 { --tw-gradient-via: var(--color-purple-500); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } - .to-indigo-600 { - --tw-gradient-to: var(--color-indigo-600); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .to-pink-500 { --tw-gradient-to: var(--color-pink-500); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -785,8 +795,8 @@ .p-2 { padding: calc(var(--spacing) * 2); } - .p-2\.5 { - padding: calc(var(--spacing) * 2.5); + .p-3 { + padding: calc(var(--spacing) * 3); } .p-4 { padding: calc(var(--spacing) * 4); @@ -809,12 +819,12 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } - .px-5 { - padding-inline: calc(var(--spacing) * 5); - } .px-6 { padding-inline: calc(var(--spacing) * 6); } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } .py-1 { padding-block: calc(var(--spacing) * 1); } @@ -824,12 +834,6 @@ .py-2\.5 { padding-block: calc(var(--spacing) * 2.5); } - .py-3 { - padding-block: calc(var(--spacing) * 3); - } - .py-3\.5 { - padding-block: calc(var(--spacing) * 3.5); - } .py-4 { padding-block: calc(var(--spacing) * 4); } @@ -848,8 +852,8 @@ .pb-24 { padding-bottom: calc(var(--spacing) * 24); } - .pl-12 { - padding-left: calc(var(--spacing) * 12); + .pl-10 { + padding-left: calc(var(--spacing) * 10); } .text-center { text-align: center; @@ -901,6 +905,10 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } .tracking-tight { --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); @@ -919,26 +927,38 @@ .text-blue-500 { color: var(--color-blue-500); } - .text-gray-200 { - color: var(--color-gray-200); + .text-card-foreground { + color: var(--color-card-foreground); } - .text-gray-400 { - color: var(--color-gray-400); + .text-destructive { + color: var(--color-destructive); + } + .text-destructive-foreground { + color: var(--color-destructive-foreground); + } + .text-foreground { + color: var(--color-foreground); } .text-gray-500 { color: var(--color-gray-500); } - .text-gray-600 { - color: var(--color-gray-600); - } - .text-gray-900 { - color: var(--color-gray-900); - } .text-green-500 { color: var(--color-green-500); } - .text-red-400 { - color: var(--color-red-400); + .text-muted-foreground { + color: var(--color-muted-foreground); + } + .text-muted-foreground\/50 { + color: color-mix(in srgb, hsl(var(--muted-foreground)) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-muted-foreground) 50%, transparent); + } + } + .text-primary { + color: var(--color-primary); + } + .text-primary-foreground { + color: var(--color-primary-foreground); } .text-red-500 { color: var(--color-red-500); @@ -946,6 +966,9 @@ .text-red-600 { color: var(--color-red-600); } + .text-secondary-foreground { + color: var(--color-secondary-foreground); + } .text-transparent { color: transparent; } @@ -958,6 +981,9 @@ .uppercase { text-transform: uppercase; } + .underline-offset-4 { + text-underline-offset: 4px; + } .opacity-5 { opacity: 5%; } @@ -989,44 +1015,28 @@ --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .ring-0 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .ring-1 { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-blue-500\/20 { - --tw-shadow-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 20%, transparent) var(--tw-shadow-alpha), transparent); - } - } .shadow-blue-500\/30 { --tw-shadow-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 30%, transparent) var(--tw-shadow-alpha), transparent); } } - .shadow-red-500\/20 { - --tw-shadow-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 20%, transparent) var(--tw-shadow-alpha), transparent); - } - } .ring-blue-500\/50 { --tw-ring-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { --tw-ring-color: color-mix(in oklab, var(--color-blue-500) 50%, transparent); } } - .ring-white\/5 { - --tw-ring-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - .backdrop-blur-md { - --tw-backdrop-blur: blur(var(--blur-md)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + .ring-offset-background { + --tw-ring-offset-color: var(--color-background); } .backdrop-blur-sm { --tw-backdrop-blur: blur(var(--blur-sm)); @@ -1074,61 +1084,9 @@ --tw-duration: 500ms; transition-duration: 500ms; } - .selection\:bg-blue-500\/20 { - & *::selection { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); - } - } - &::selection { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); - } - } - } - .selection\:bg-blue-500\/30 { - & *::selection { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 30%, transparent); - } - } - &::selection { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 30%, transparent); - } - } - } - .selection\:bg-blue-600\/40 { - & *::selection { - background-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-600) 40%, transparent); - } - } - &::selection { - background-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-600) 40%, transparent); - } - } - } - .placeholder\:text-gray-600 { + .placeholder\:text-muted-foreground { &::placeholder { - color: var(--color-gray-600); - } - } - .hover\:scale-105 { - &:hover { - @media (hover: hover) { - --tw-scale-x: 105%; - --tw-scale-y: 105%; - --tw-scale-z: 105%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } + color: var(--color-muted-foreground); } } .hover\:border-gray-500\/30 { @@ -1141,34 +1099,40 @@ } } } - .hover\:border-white\/10 { + .hover\:bg-accent { &:hover { @media (hover: hover) { - border-color: color-mix(in srgb, #fff 10%, transparent); + background-color: var(--color-accent); + } + } + } + .hover\:bg-destructive\/90 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, hsl(var(--destructive)) 90%, transparent); @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 10%, transparent); + background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent); } } } } - .hover\:bg-blue-600 { + .hover\:bg-muted\/50 { &:hover { @media (hover: hover) { - background-color: var(--color-blue-600); + background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-muted) 50%, transparent); + } } } } - .hover\:bg-gray-100 { + .hover\:bg-primary\/90 { &:hover { @media (hover: hover) { - background-color: var(--color-gray-100); - } - } - } - .hover\:bg-gray-900 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-900); + background-color: color-mix(in srgb, hsl(var(--primary)) 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 90%, transparent); + } } } } @@ -1182,13 +1146,6 @@ } } } - .hover\:bg-red-600 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-600); - } - } - } .hover\:bg-red-900\/20 { &:hover { @media (hover: hover) { @@ -1199,12 +1156,12 @@ } } } - .hover\:bg-white\/5 { + .hover\:bg-secondary\/80 { &:hover { @media (hover: hover) { - background-color: color-mix(in srgb, #fff 5%, transparent); + background-color: color-mix(in srgb, hsl(var(--secondary)) 80%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); + background-color: color-mix(in oklab, var(--color-secondary) 80%, transparent); } } } @@ -1219,10 +1176,17 @@ } } } - .hover\:text-gray-300 { + .hover\:text-accent-foreground { &:hover { @media (hover: hover) { - color: var(--color-gray-300); + color: var(--color-accent-foreground); + } + } + } + .hover\:text-foreground { + &:hover { + @media (hover: hover) { + color: var(--color-foreground); } } } @@ -1233,10 +1197,10 @@ } } } - .hover\:text-white { + .hover\:underline { &:hover { @media (hover: hover) { - color: var(--color-white); + text-decoration-line: underline; } } } @@ -1247,35 +1211,9 @@ } } } - .hover\:shadow-\[0_0_20px_rgba\(59\,130\,246\,0\.3\)\] { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 0 20px var(--tw-shadow-color, rgba(59,130,246,0.3)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:shadow-lg { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:shadow-blue-500\/30 { - &:hover { - @media (hover: hover) { - --tw-shadow-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-blue-500) 30%, transparent) var(--tw-shadow-alpha), transparent); - } - } - } - } - .focus\:border-blue-500 { + .focus\:border-ring { &:focus { - border-color: var(--color-blue-500); + border-color: var(--color-ring); } } .focus\:ring-1 { @@ -1284,9 +1222,9 @@ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } } - .focus\:ring-blue-500 { + .focus\:ring-ring { &:focus { - --tw-ring-color: var(--color-blue-500); + --tw-ring-color: var(--color-ring); } } .focus\:outline-none { @@ -1295,12 +1233,27 @@ outline-style: none; } } - .active\:scale-95 { - &:active { - --tw-scale-x: 95%; - --tw-scale-y: 95%; - --tw-scale-z: 95%; - scale: var(--tw-scale-x) var(--tw-scale-y); + .focus-visible\:ring-2 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus-visible\:ring-ring { + &:focus-visible { + --tw-ring-color: var(--color-ring); + } + } + .focus-visible\:ring-offset-2 { + &:focus-visible { + --tw-ring-offset-width: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + } + } + .focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; } } .active\:scale-\[0\.98\] { @@ -1308,6 +1261,16 @@ scale: 0.98; } } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } .sm\:grid-cols-3 { @media (width >= 40rem) { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1358,9 +1321,9 @@ align-items: center; } } - .md\:rounded-2xl { + .md\:rounded-lg { @media (width >= 48rem) { - border-radius: var(--radius-2xl); + border-radius: var(--radius-lg); } } .md\:pt-6 { @@ -1387,6 +1350,75 @@ touch-action: manipulation; } } +@layer base { + :root { + --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 10% 3.9%; + --radius: 0.5rem; + } + .dark { + --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%; + } + .amoled { + --background: 0 0% 0%; + --foreground: 0 0% 98%; + --card: 0 0% 0%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 0%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 0%; + --secondary: 0 0% 9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 9%; + --muted-foreground: 0 0% 60%; + --accent: 0 0% 9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 12%; + --input: 0 0% 12%; + --ring: 0 0% 83.9%; + } + * { + border-color: var(--color-border); + } +} @property --tw-translate-x { syntax: "*"; inherits: false; @@ -1592,21 +1624,6 @@ syntax: "*"; inherits: false; } -@property --tw-scale-x { - syntax: "*"; - inherits: false; - initial-value: 1; -} -@property --tw-scale-y { - syntax: "*"; - inherits: false; - initial-value: 1; -} -@property --tw-scale-z { - syntax: "*"; - inherits: false; - initial-value: 1; -} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -1656,9 +1673,6 @@ --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; --tw-duration: initial; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-scale-z: 1; } } } diff --git a/frontend/src/app.rs b/frontend/src/app.rs index e650e66..dec37de 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -2,6 +2,7 @@ use leptos::*; use shared::{Torrent, AppEvent, TorrentStatus, Theme}; use crate::components::context_menu::ContextMenu; use crate::components::modal::Modal; +use crate::components::ui::button::{Button, ButtonVariant}; use gloo_net::eventsource::futures::EventSource; use futures::StreamExt; @@ -26,12 +27,34 @@ pub fn App() -> impl IntoView { }); // Persist Theme + // Persist Theme & Apply CSS Variables create_effect(move |_| { let val = match theme.get() { Theme::Midnight => "Midnight", Theme::Light => "Light", Theme::Amoled => "Amoled", }; + + if let Some(doc) = window().document() { + if let Some(body) = doc.body() { + let list = body.class_list(); + match theme.get() { + Theme::Light => { + let _ = list.remove_1("dark"); + let _ = list.remove_1("amoled"); + }, + Theme::Midnight => { + let _ = list.add_1("dark"); + let _ = list.remove_1("amoled"); + }, + Theme::Amoled => { + let _ = list.add_1("dark"); + let _ = list.add_1("amoled"); + }, + } + } + } + if let Some(storage) = window().local_storage().ok().flatten() { let _ = storage.set_item("vibetorrent_theme", val); } @@ -195,62 +218,32 @@ pub fn App() -> impl IntoView { }; // Theme Engine - let get_theme_classes = move || { - match theme.get() { - Theme::Midnight => ( - "bg-[#0a0a0c] text-white selection:bg-blue-500/30", // Main bg - "bg-[#111116]/80 backdrop-blur-xl border-white/5", // Sidebar - "bg-[#111116] border-white/5 shadow-2xl", // Card/Table bg - "text-gray-200", // Primary Text - "text-gray-400", // Secondary Text - "hover:bg-white/5", // Hover - "border-white/5" // Border - ), - Theme::Light => ( - "bg-gray-50 text-gray-900 selection:bg-blue-500/20", - "bg-white/80 backdrop-blur-xl border-gray-200", - "bg-white border-gray-200 shadow-xl", - "text-gray-900", - "text-gray-500", - "hover:bg-gray-100", - "border-gray-200" - ), - Theme::Amoled => ( - "bg-black text-white selection:bg-blue-600/40", - "bg-black border-gray-800", - "bg-black border-gray-800", - "text-gray-200", - "text-gray-500", - "hover:bg-gray-900", - "border-gray-800" - ), - } - }; + let filter_btn_class = move |status: Option| { - let (_base_bg, _, _, _, text_sec, hover, _) = get_theme_classes(); - let base = "block px-4 py-2 rounded-xl transition-all duration-200 text-left w-full flex items-center gap-3 border"; - let active = filter_status.get() == status; - if active { - format!("{} bg-blue-600/20 text-blue-500 border-blue-500/30 font-medium", base) - } else { - format!("{} {} {} border-transparent hover:text-gray-300", base, hover, text_sec) - } + crate::utils::cn(format!( + "block px-4 py-2 rounded-md transition-all duration-200 text-left w-full flex items-center gap-3 border text-sm font-medium {}", + if filter_status.get() == status { + "bg-primary/10 text-primary border-primary/20" + } else { + "border-transparent text-muted-foreground hover:text-foreground hover:bg-accent hover:text-accent-foreground" + } + )) }; let tab_btn_class = move |tab: &str| { - let active = active_tab.get() == tab; - let base = "flex flex-col items-center justify-center p-2 flex-1 transition-colors relative"; - if active { - format!("{} text-blue-500", base) - } else { - "flex flex-col items-center justify-center p-2 flex-1 transition-colors relative text-gray-400 hover:text-gray-300".to_string() - } + crate::utils::cn(format!( + "flex flex-col items-center justify-center p-2 flex-1 transition-colors relative {}", + if active_tab.get() == tab { + "text-primary" + } else { + "text-muted-foreground hover:text-foreground" + } + )) }; // Sidebar Content Logic let sidebar_content = move || { - let (_, _, _, _text_pri, text_sec, _, border) = get_theme_classes(); view! {
@@ -263,7 +256,7 @@ pub fn App() -> impl IntoView {
-
"Filters"
+
"Filters"
-
-
-
-
"Storage"
-
+
+
+
+
"Storage"
+
-
+
"700 GB used" "1 TB total"
@@ -333,379 +326,373 @@ pub fn App() -> impl IntoView { }; view! { - {move || { - let (main_bg, sidebar_bg, card_bg, text_pri, text_sec, hover, border) = get_theme_classes(); +
+ // DESKTOP SIDEBAR + - view! { -
- // DESKTOP SIDEBAR - - - // MOBILE SIDEBAR -
-
- + // MOBILE SIDEBAR +
+
+ +
+ + // MAIN CONTENT +
+
+
+ +

+ {move || if active_tab.get() == "settings" { "Settings" } else if active_tab.get() == "dashboard" { "Dashboard" } else { + match filter_status.get() { + None => "All Torrents", + Some(TorrentStatus::Downloading) => "Downloading", + Some(TorrentStatus::Seeding) => "Seeding", + Some(TorrentStatus::Paused) => "Paused", + Some(TorrentStatus::Error) => "Errors", + _ => "Torrents" + } + }} +

+
+ + + + +
+
- // MAIN CONTENT -
-
-
- -

- {move || if active_tab.get() == "settings" { "Settings" } else if active_tab.get() == "dashboard" { "Dashboard" } else { - match filter_status.get() { - None => "All Torrents", - Some(TorrentStatus::Downloading) => "Downloading", - Some(TorrentStatus::Seeding) => "Seeding", - Some(TorrentStatus::Paused) => "Paused", - Some(TorrentStatus::Error) => "Errors", - _ => "Torrents" - } - }} -

-
-
-
- "Server Time: " - {move || { - let ts = last_updated.get(); - if ts == 0 { - "Waiting...".to_string() - } else { - let s = ts % 60; - let m = (ts / 60) % 60; - let h = (ts / 3600) % 24; - format!("{:02}:{:02}:{:02} UTC", h, m, s) - } - }} +
+ {move || if active_tab.get() == "settings" { + view! { +
+
+

"Appearance"

+
+ {theme_option(Theme::Midnight, "Midnight", "bg-[#0a0a0c] border border-gray-700")} + {theme_option(Theme::Light, "Light", "bg-gray-100 border border-gray-300")} + {theme_option(Theme::Amoled, "Amoled", "bg-black border border-gray-800")} +
+
+
+

"About VibeTorrent"

+

"Version 3.0.0 (Rust + WebAssembly)"

- - -
-
- -
- {move || if active_tab.get() == "settings" { - view! { -
-
-

"Appearance"

-
- {theme_option(Theme::Midnight, "Midnight", "bg-[#0a0a0c] border border-gray-700")} - {theme_option(Theme::Light, "Light", "bg-gray-100 border border-gray-300")} - {theme_option(Theme::Amoled, "Amoled", "bg-black border border-gray-800")} -
-
-
-

"About VibeTorrent"

-

"Version 3.0.0 (Rust + WebAssembly)"

-
-
- }.into_view() - } else if active_tab.get() == "dashboard" { - view! { -
"Dashboard Charts Coming Soon..."
- }.into_view() - } else { - view! { - // Torrent List (Desktop) -
- - - - - - - - - - - - - - "text-blue-500 bg-blue-500/10 border-blue-500/20", - TorrentStatus::Seeding => "text-green-500 bg-green-500/10 border-green-500/20", - TorrentStatus::Paused => "text-yellow-500 bg-yellow-500/10 border-yellow-500/20", - TorrentStatus::Error => "text-red-500 bg-red-500/10 border-red-500/20", - _ => "text-gray-400 bg-gray-500/10" - }; - let status_text = format!("{:?}", torrent.status); - let error_msg = torrent.error_message.clone(); - let error_msg_view = error_msg.clone(); - - view! { - - - - - - - - - - } - } - /> - -
"Name""Size""Progress""Down""Up""ETA""Status"
-
- {torrent.name} -
- -
{error_msg_view.clone()}
-
-
{format_bytes(torrent.size)} -
-
- {format!("{:.1}%", torrent.percent_complete)} -
-
-
-
-
-
- {if torrent.down_rate > 0 { - view! { {format_bytes(torrent.down_rate)} "/s" }.into_view() - } else { - view! { "-" }.into_view() - }} - - {if torrent.up_rate > 0 { - view! { {format_bytes(torrent.up_rate)} "/s" }.into_view() - } else { - view! { "-" }.into_view() - }} - - {format_eta(torrent.eta)} - - - {status_text} - -
-
- - // Torrent List (Mobile) -
+ }.into_view() + } else if active_tab.get() == "dashboard" { + view! { +
"Dashboard Charts Coming Soon..."
+ }.into_view() + } else { + view! { + // Torrent List (Desktop) + + + // Torrent List (Mobile) +
+ "text-blue-500", + TorrentStatus::Seeding => "text-green-500", + TorrentStatus::Paused => "text-yellow-500", + TorrentStatus::Error => "text-destructive", + _ => "text-muted-foreground" + }; + + view! { +
+
+
{torrent.name}
+
+ {format!("{:?}", torrent.status)} +
+
+
+
+ {format_bytes(torrent.size)} + {format!("{:.1}%", torrent.percent_complete)} +
+
+
+
+
+
+
+ "↓ " {format_bytes(torrent.down_rate)} "/s" + "↑ " {format_bytes(torrent.up_rate)} "/s" +
+
{format_eta(torrent.eta)}
+
- + } + } + /> + +
+
"📭"
+ "No torrents found."
- }.into_view() - }} -
-
- - // MOBILE BOTTOM NAV - + +
+ }.into_view() + }} +
+ - // Modal (Dark backdrop always) - -
-
-
-

"Add New Torrent"

- -
-
- -
- -
-
-
- -
+ // MOBILE BOTTOM NAV + + + // Modal (Dark backdrop always) + +
+
+
+

"Add New Torrent"

+ +
+
+ +
+
- - - - - // Delete Confirmation Modal - -

"Are you definitely sure you want to delete this torrent?"

- -

"⚠️ This will also permanently delete the downloaded files from the disk."

-
-
+
+ +
+
- } - }} +
+ + + + // Delete Confirmation Modal + +

"Are you definitely sure you want to delete this torrent?"

+ +

"⚠️ This will also permanently delete the downloaded files from the disk."

+
+
+
} } diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index 86532a9..4c7e7cd 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -1,2 +1,4 @@ -pub mod context_menu; pub mod modal; +pub mod context_menu; +pub mod ui; + diff --git a/frontend/src/components/modal.rs b/frontend/src/components/modal.rs index 3ea3f0f..05f5746 100644 --- a/frontend/src/components/modal.rs +++ b/frontend/src/components/modal.rs @@ -21,25 +21,26 @@ pub fn Modal( view! { -
-
-

{title.get_value()}

+
+
+

{title.get_value()}

-
+
{child_view.with_value(|c| c.clone())}
-
+
+ } +} diff --git a/frontend/src/components/ui/mod.rs b/frontend/src/components/ui/mod.rs new file mode 100644 index 0000000..aa200ca --- /dev/null +++ b/frontend/src/components/ui/mod.rs @@ -0,0 +1 @@ +pub mod button; diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index ab22239..38b88a7 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,6 +1,7 @@ mod app; // mod models; // Removed mod components; +pub mod utils; use leptos::*; use wasm_bindgen::prelude::*; diff --git a/frontend/src/utils/mod.rs b/frontend/src/utils/mod.rs new file mode 100644 index 0000000..197ad25 --- /dev/null +++ b/frontend/src/utils/mod.rs @@ -0,0 +1,5 @@ +use tailwind_fuse::merge::tw_merge; + +pub fn cn(classes: impl AsRef) -> String { + tw_merge(classes.as_ref()) +}