diff --git a/Cargo.lock b/Cargo.lock index b57c6f8..4e11116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "async-compression" version = "0.4.37" @@ -314,6 +323,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "byteorder" version = "1.5.0" @@ -644,6 +659,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "find-msvc-tools" version = "0.1.8" @@ -690,6 +711,7 @@ dependencies = [ "serde_json", "shared", "tailwind_fuse", + "thaw", "uuid", "wasm-bindgen", "web-sys", @@ -1035,6 +1057,21 @@ dependencies = [ "cc", ] +[[package]] +name = "icondata_ai" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3a9c196a6a169f790639ecc8fdd4396660b1d53b905230bf0b364776a56fc" +dependencies = [ + "icondata_core", +] + +[[package]] +name = "icondata_core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859" + [[package]] name = "icu_collections" version = "2.1.1" @@ -1526,6 +1563,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1567,6 +1628,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2142,6 +2245,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.11" @@ -2235,6 +2344,52 @@ dependencies = [ "nom", ] +[[package]] +name = "thaw" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2ea14de27ddd0ce167c69e78852122c5a9c755b3c083c8249e491c4a8821ce" +dependencies = [ + "cfg-if", + "chrono", + "icondata_ai", + "icondata_core", + "leptos", + "num-traits", + "palette", + "thaw_components", + "thaw_utils", + "uuid", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thaw_components" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ee9d9598f7a5162845a83ef0ed3ccde52c23d987cf64433056b2fd4542161c" +dependencies = [ + "cfg-if", + "leptos", + "thaw_utils", + "uuid", + "web-sys", +] + +[[package]] +name = "thaw_utils" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5fe5c99ba6b6730ec533810dd4d92422b778d4fbaffa9b6d7fb705b28b3978" +dependencies = [ + "cfg-if", + "chrono", + "leptos", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index c6f3d90..ca6b0ab 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -21,3 +21,4 @@ chrono = { version = "0.4", features = ["serde"] } web-sys = { version = "0.3", features = ["Window", "Storage"] } shared = { path = "../shared" } tailwind_fuse = "0.3.2" +thaw = "0.3" diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css index 2e00139..47ea9c8 100644 --- a/frontend/public/tailwind.css +++ b/frontend/public/tailwind.css @@ -14,39 +14,22 @@ --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-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-100: oklch(96.7% 0.003 264.542); - --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-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; --container-sm: 24rem; - --container-lg: 32rem; - --container-7xl: 80rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); - --text-xl: 1.25rem; - --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); - --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; @@ -63,8 +46,6 @@ --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)); @@ -237,238 +218,102 @@ .fixed { position: fixed; } - .relative { - position: relative; + .sticky { + position: sticky; } .inset-0 { inset: calc(var(--spacing) * 0); } - .inset-x-0 { - inset-inline: calc(var(--spacing) * 0); - } .top-0 { top: calc(var(--spacing) * 0); } - .top-1\/2 { - top: calc(1/2 * 100%); - } - .top-4 { - top: calc(var(--spacing) * 4); - } - .right-0 { - right: calc(var(--spacing) * 0); - } - .right-4 { - right: calc(var(--spacing) * 4); - } - .bottom-0 { - bottom: calc(var(--spacing) * 0); - } - .left-0 { - left: calc(var(--spacing) * 0); - } - .left-3 { - left: calc(var(--spacing) * 3); - } .z-10 { z-index: 10; } - .z-20 { - z-index: 20; - } - .z-30 { - z-index: 30; - } - .z-40 { - z-index: 40; - } - .z-50 { - z-index: 50; - } .z-\[100\] { z-index: 100; } .z-\[200\] { z-index: 200; } - .mx-auto { - margin-inline: auto; + .mx-2 { + margin-inline: calc(var(--spacing) * 2); } .my-1 { margin-block: calc(var(--spacing) * 1); } - .mt-1 { - margin-top: calc(var(--spacing) * 1); - } - .mt-2 { - margin-top: calc(var(--spacing) * 2); - } - .mt-10 { - margin-top: calc(var(--spacing) * 10); - } - .mt-auto { - margin-top: auto; + .mr-2 { + margin-right: calc(var(--spacing) * 2); } .mb-1 { margin-bottom: calc(var(--spacing) * 1); } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); - } - .mb-3 { - margin-bottom: calc(var(--spacing) * 3); - } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } .mb-6 { margin-bottom: calc(var(--spacing) * 6); } - .mb-10 { - margin-bottom: calc(var(--spacing) * 10); - } - .-ml-2 { - margin-left: calc(var(--spacing) * -2); - } - .ml-auto { - margin-left: auto; - } - .line-clamp-2 { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - } - .block { - display: block; - } .flex { display: flex; } - .grid { - display: grid; - } - .hidden { - display: none; - } .inline-flex { display: inline-flex; } - .h-1\.5 { - height: calc(var(--spacing) * 1.5); - } - .h-2 { - height: calc(var(--spacing) * 2); - } .h-4 { height: calc(var(--spacing) * 4); } - .h-5 { - height: calc(var(--spacing) * 5); - } - .h-6 { - height: calc(var(--spacing) * 6); - } - .h-9 { - height: calc(var(--spacing) * 9); + .h-8 { + height: calc(var(--spacing) * 8); } .h-10 { height: calc(var(--spacing) * 10); } - .h-11 { - height: calc(var(--spacing) * 11); - } - .h-12 { - height: calc(var(--spacing) * 12); - } - .h-full { - height: 100%; - } .h-px { height: 1px; } .h-screen { height: 100vh; } - .min-h-screen { - min-height: 100vh; - } - .w-2 { - width: calc(var(--spacing) * 2); - } .w-4 { width: calc(var(--spacing) * 4); } - .w-5 { - width: calc(var(--spacing) * 5); + .w-20 { + width: calc(var(--spacing) * 20); } - .w-6 { - width: calc(var(--spacing) * 6); + .w-24 { + width: calc(var(--spacing) * 24); } - .w-10 { - width: calc(var(--spacing) * 10); + .w-48 { + width: calc(var(--spacing) * 48); } - .w-12 { - width: calc(var(--spacing) * 12); - } - .w-28 { - width: calc(var(--spacing) * 28); - } - .w-36 { - width: calc(var(--spacing) * 36); - } - .w-72 { - width: calc(var(--spacing) * 72); - } - .w-80 { - width: calc(var(--spacing) * 80); - } - .w-\[70\%\] { - width: 70%; + .w-64 { + width: calc(var(--spacing) * 64); } .w-full { width: 100%; } - .max-w-7xl { - max-width: var(--container-7xl); + .w-px { + width: 1px; } - .max-w-\[85vw\] { - max-width: 85vw; - } - .max-w-lg { - max-width: var(--container-lg); + .max-w-\[200px\] { + max-width: 200px; } .max-w-sm { max-width: var(--container-sm); } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } .min-w-\[200px\] { min-width: 200px; } .flex-1 { flex: 1; } - .flex-shrink-0 { - flex-shrink: 0; - } - .table-fixed { - table-layout: fixed; - } - .-translate-y-1\/2 { - --tw-translate-y: calc(calc(1/2 * 100%) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } - .cursor-default { - cursor: default; - } - .cursor-pointer { - cursor: pointer; - } - .grid-cols-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } .flex-col { flex-direction: column; } @@ -478,20 +323,17 @@ .items-end { align-items: flex-end; } - .items-start { - align-items: flex-start; - } - .justify-between { - justify-content: space-between; - } .justify-center { justify-content: center; } .justify-end { justify-content: flex-end; } - .gap-1\.5 { - gap: calc(var(--spacing) * 1.5); + .justify-start { + justify-content: flex-start; + } + .gap-1 { + gap: calc(var(--spacing) * 1); } .gap-2 { gap: calc(var(--spacing) * 2); @@ -502,11 +344,11 @@ .gap-4 { gap: calc(var(--spacing) * 4); } - .space-y-2 { + .space-y-1 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); } } .space-y-4 { @@ -516,11 +358,11 @@ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } - .space-y-8 { + .space-x-4 { :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); } } .divide-y { @@ -542,18 +384,15 @@ text-overflow: ellipsis; white-space: nowrap; } + .overflow-auto { + overflow: auto; + } .overflow-hidden { overflow: hidden; } - .overflow-x-hidden { - overflow-x: hidden; - } .overflow-y-auto { overflow-y: auto; } - .rounded-2xl { - border-radius: var(--radius-2xl); - } .rounded-full { border-radius: calc(infinity * 1px); } @@ -583,66 +422,18 @@ border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; } - .border-blue-500 { - border-color: var(--color-blue-500); - } - .border-blue-500\/20 { - border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); - } - } .border-border { border-color: var(--color-border); } - .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); - } - .border-gray-700 { - border-color: var(--color-gray-700); - } - .border-gray-800 { - border-color: var(--color-gray-800); - } - .border-green-500\/20 { - border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-green-500) 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-primary) 20%, transparent); - } - } - .border-transparent { - border-color: transparent; - } .border-white\/10 { border-color: color-mix(in srgb, #fff 10%, transparent); @supports (color: color-mix(in lab, red, red)) { border-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } - .border-yellow-500\/20 { - border-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-yellow-500) 20%, transparent); - } - } - .bg-\[\#0a0a0c\] { - background-color: #0a0a0c; - } .bg-\[\#111116\]\/95 { background-color: color-mix(in oklab, #111116 95%, transparent); } @@ -655,66 +446,30 @@ background-color: color-mix(in oklab, var(--color-background) 80%, transparent); } } - .bg-black { - background-color: var(--color-black); - } - .bg-black\/5 { - background-color: color-mix(in srgb, #000 5%, transparent); + .bg-background\/95 { + background-color: color-mix(in srgb, hsl(var(--background)) 95%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 5%, transparent); + background-color: color-mix(in oklab, var(--color-background) 95%, transparent); } } - .bg-blue-500 { - background-color: var(--color-blue-500); - } - .bg-blue-500\/10 { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 10%, transparent); - } + .bg-border { + background-color: var(--color-border); } .bg-card { background-color: var(--color-card); } - .bg-card\/50 { - background-color: color-mix(in srgb, hsl(var(--card)) 50%, transparent); + .bg-card\/30 { + background-color: color-mix(in srgb, hsl(var(--card)) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-card) 50%, transparent); + background-color: color-mix(in oklab, var(--color-card) 30%, transparent); } } .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); - } - .bg-gray-400 { - background-color: var(--color-gray-400); - } - .bg-green-500 { - background-color: var(--color-green-500); - } - .bg-green-500\/10 { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - 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)) { @@ -724,137 +479,48 @@ .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-secondary { - background-color: var(--color-secondary); - } - .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-secondary) 50%, transparent); - } - } .bg-white\/10 { background-color: color-mix(in srgb, #fff 10%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } - .bg-yellow-500 { - background-color: var(--color-yellow-500); - } - .bg-yellow-500\/10 { - background-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-yellow-500) 10%, transparent); - } - } - .bg-gradient-to-br { - --tw-gradient-position: to bottom right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - .bg-gradient-to-r { - --tw-gradient-position: to right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - .from-blue-500 { - --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)); - } - .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-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)); - } - .to-purple-500 { - --tw-gradient-to: var(--color-purple-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)); - } - .to-purple-600 { - --tw-gradient-to: var(--color-purple-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)); - } - .bg-clip-text { - background-clip: text; - } - .p-1 { - padding: calc(var(--spacing) * 1); - } .p-2 { padding: calc(var(--spacing) * 2); } - .p-3 { - padding: calc(var(--spacing) * 3); - } .p-4 { padding: calc(var(--spacing) * 4); } .p-6 { padding: calc(var(--spacing) * 6); } - .p-12 { - padding: calc(var(--spacing) * 12); + .px-1\.5 { + padding-inline: calc(var(--spacing) * 1.5); } .px-2 { padding-inline: calc(var(--spacing) * 2); } - .px-2\.5 { - padding-inline: calc(var(--spacing) * 2.5); - } .px-3 { padding-inline: calc(var(--spacing) * 3); } .px-4 { padding-inline: calc(var(--spacing) * 4); } - .px-6 { - padding-inline: calc(var(--spacing) * 6); - } - .px-8 { - padding-inline: calc(var(--spacing) * 8); + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); } .py-1 { padding-block: calc(var(--spacing) * 1); } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } .py-2 { padding-block: calc(var(--spacing) * 2); } .py-2\.5 { padding-block: calc(var(--spacing) * 2.5); } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .py-20 { - padding-block: calc(var(--spacing) * 20); - } - .pt-6 { - padding-top: calc(var(--spacing) * 6); - } - .pt-\[88px\] { - padding-top: 88px; - } - .pr-4 { - padding-right: calc(var(--spacing) * 4); - } - .pb-24 { - padding-bottom: calc(var(--spacing) * 24); - } - .pl-10 { - padding-left: calc(var(--spacing) * 10); - } .text-center { text-align: center; } @@ -864,9 +530,6 @@ .text-right { text-align: right; } - .font-mono { - font-family: var(--font-mono); - } .font-sans { font-family: var(--font-sans); } @@ -874,10 +537,6 @@ font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } - .text-6xl { - font-size: var(--text-6xl); - line-height: var(--tw-leading, var(--text-6xl--line-height)); - } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -886,10 +545,6 @@ font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } .text-xs { font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); @@ -909,18 +564,10 @@ --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); - } .tracking-wider { --tw-tracking: var(--tracking-wider); letter-spacing: var(--tracking-wider); } - .tracking-widest { - --tw-tracking: var(--tracking-widest); - letter-spacing: var(--tracking-widest); - } .whitespace-nowrap { white-space: nowrap; } @@ -930,9 +577,6 @@ .text-card-foreground { color: var(--color-card-foreground); } - .text-destructive { - color: var(--color-destructive); - } .text-destructive-foreground { color: var(--color-destructive-foreground); } @@ -948,15 +592,6 @@ .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); } @@ -966,12 +601,6 @@ .text-red-600 { color: var(--color-red-600); } - .text-secondary-foreground { - color: var(--color-secondary-foreground); - } - .text-transparent { - color: transparent; - } .text-white { color: var(--color-white); } @@ -981,36 +610,10 @@ .uppercase { text-transform: uppercase; } - .underline-offset-4 { - text-underline-offset: 4px; - } - .opacity-5 { - opacity: 5%; - } - .opacity-20 { - opacity: 20%; - } - .opacity-50 { - opacity: 50%; - } - .opacity-60 { - opacity: 60%; - } - .opacity-80 { - opacity: 80%; - } .shadow-2xl { --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-lg { - --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); - } - .shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px 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); - } .shadow-xl { --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); @@ -1019,25 +622,14 @@ --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\/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); - } - } - .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-offset-background { --tw-ring-offset-color: var(--color-background); } + .backdrop-blur { + --tw-backdrop-blur: blur(8px); + -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,); + } .backdrop-blur-sm { --tw-backdrop-blur: blur(var(--blur-sm)); -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,); @@ -1058,16 +650,6 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .transition-opacity { - transition-property: opacity; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-transform { - transition-property: transform, translate, scale, rotate; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } .duration-100 { --tw-duration: 100ms; transition-duration: 100ms; @@ -1076,29 +658,6 @@ --tw-duration: 200ms; transition-duration: 200ms; } - .duration-300 { - --tw-duration: 300ms; - transition-duration: 300ms; - } - .duration-500 { - --tw-duration: 500ms; - transition-duration: 500ms; - } - .placeholder\:text-muted-foreground { - &::placeholder { - color: var(--color-muted-foreground); - } - } - .hover\:border-gray-500\/30 { - &:hover { - @media (hover: hover) { - border-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-gray-500) 30%, transparent); - } - } - } - } .hover\:bg-accent { &:hover { @media (hover: hover) { @@ -1156,16 +715,6 @@ } } } - .hover\:bg-secondary\/80 { - &:hover { - @media (hover: hover) { - 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-secondary) 80%, transparent); - } - } - } - } .hover\:bg-white\/10 { &:hover { @media (hover: hover) { @@ -1183,13 +732,6 @@ } } } - .hover\:text-foreground { - &:hover { - @media (hover: hover) { - color: var(--color-foreground); - } - } - } .hover\:text-red-400 { &:hover { @media (hover: hover) { @@ -1197,25 +739,6 @@ } } } - .hover\:underline { - &:hover { - @media (hover: hover) { - text-decoration-line: underline; - } - } - } - .hover\:opacity-80 { - &:hover { - @media (hover: hover) { - opacity: 80%; - } - } - } - .focus\:border-ring { - &:focus { - border-color: var(--color-ring); - } - } .focus\:ring-1 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -1256,11 +779,6 @@ outline-style: none; } } - .active\:scale-\[0\.98\] { - &:active { - scale: 0.98; - } - } .disabled\:pointer-events-none { &:disabled { pointer-events: none; @@ -1271,9 +789,12 @@ opacity: 50%; } } - .sm\:grid-cols-3 { - @media (width >= 40rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); + .supports-\[backdrop-filter\]\:bg-background\/60 { + @supports (backdrop-filter: var(--tw)) { + background-color: color-mix(in srgb, hsl(var(--background)) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-background) 60%, transparent); + } } } .sm\:p-4 { @@ -1281,41 +802,6 @@ padding: calc(var(--spacing) * 4); } } - .md\:sticky { - @media (width >= 48rem) { - position: sticky; - } - } - .md\:top-0 { - @media (width >= 48rem) { - top: calc(var(--spacing) * 0); - } - } - .md\:block { - @media (width >= 48rem) { - display: block; - } - } - .md\:flex { - @media (width >= 48rem) { - display: flex; - } - } - .md\:hidden { - @media (width >= 48rem) { - display: none; - } - } - .md\:inline { - @media (width >= 48rem) { - display: inline; - } - } - .md\:flex-row { - @media (width >= 48rem) { - flex-direction: row; - } - } .md\:items-center { @media (width >= 48rem) { align-items: center; @@ -1326,24 +812,6 @@ border-radius: var(--radius-lg); } } - .md\:pt-6 { - @media (width >= 48rem) { - padding-top: calc(var(--spacing) * 6); - } - } - .md\:pb-0 { - @media (width >= 48rem) { - padding-bottom: calc(var(--spacing) * 0); - } - } - .dark\:bg-white\/5 { - @media (prefers-color-scheme: dark) { - background-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - } } @layer base { html, body, button, a, [role="button"], input, label, select, summary, textarea { @@ -1419,21 +887,6 @@ border-color: var(--color-border); } } -@property --tw-translate-x { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-y { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-z { - syntax: "*"; - inherits: false; - initial-value: 0; -} @property --tw-rotate-x { syntax: "*"; inherits: false; @@ -1459,6 +912,11 @@ inherits: false; initial-value: 0; } +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-divide-y-reverse { syntax: "*"; inherits: false; @@ -1469,48 +927,6 @@ inherits: false; initial-value: solid; } -@property --tw-gradient-position { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-via { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-to { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-via-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from-position { - syntax: ""; - inherits: false; - initial-value: 0%; -} -@property --tw-gradient-via-position { - syntax: ""; - inherits: false; - initial-value: 50%; -} -@property --tw-gradient-to-position { - syntax: ""; - inherits: false; - initial-value: 100%; -} @property --tw-font-weight { syntax: "*"; inherits: false; @@ -1627,26 +1043,15 @@ @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 { - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-translate-z: 0; --tw-rotate-x: initial; --tw-rotate-y: initial; --tw-rotate-z: initial; --tw-skew-x: initial; --tw-skew-y: initial; --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; --tw-divide-y-reverse: 0; --tw-border-style: solid; - --tw-gradient-position: initial; - --tw-gradient-from: #0000; - --tw-gradient-via: #0000; - --tw-gradient-to: #0000; - --tw-gradient-stops: initial; - --tw-gradient-via-stops: initial; - --tw-gradient-from-position: 0%; - --tw-gradient-via-position: 50%; - --tw-gradient-to-position: 100%; --tw-font-weight: initial; --tw-tracking: initial; --tw-shadow: 0 0 #0000; diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 3e2c2dc..1840f18 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,8 +1,12 @@ use leptos::*; +use thaw::*; 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 crate::components::toolbar::Toolbar; +use crate::components::sidebar::Sidebar; +// use crate::components::context_menu::ContextMenu; +// use crate::components::modal::Modal; +use crate::components::status_bar::StatusBar; +use crate::components::torrent_table::TorrentTable; use gloo_net::eventsource::futures::EventSource; use futures::StreamExt; @@ -14,7 +18,7 @@ pub fn App() -> impl IntoView { let (sort_asc, set_sort_asc) = create_signal(false); // Descending (Newest first) let (filter_status, set_filter_status) = create_signal(Option::::None); let (active_tab, set_active_tab) = create_signal("torrents"); - let (show_mobile_sidebar, set_show_mobile_sidebar) = create_signal(false); + // Theme with Persistence let (theme, set_theme) = create_signal({ let storage = window().local_storage().ok().flatten(); @@ -25,9 +29,8 @@ pub fn App() -> impl IntoView { _ => Theme::Midnight, } }); - - // Persist Theme - // Persist Theme & Apply CSS Variables + + // Persist Theme Logic create_effect(move |_| { let val = match theme.get() { Theme::Midnight => "Midnight", @@ -38,18 +41,16 @@ pub fn App() -> impl IntoView { if let Some(doc) = window().document() { if let Some(body) = doc.body() { let list = body.class_list(); + // Reset classes + let _ = list.remove_1("dark"); + let _ = list.remove_1("amoled"); + 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"); + Theme::Light => {}, + Theme::Midnight => { let _ = list.add_1("dark"); }, + Theme::Amoled => { + let _ = list.add_1("dark"); + let _ = list.add_1("amoled"); }, } } @@ -60,7 +61,7 @@ pub fn App() -> impl IntoView { } }); - // Remove Loading Spinner (Fix for spinner hanging) + // Remove Loading Spinner create_effect(move |_| { if let Some(doc) = window().document() { if let Some(el) = doc.get_element_by_id("app-loading") { @@ -69,29 +70,6 @@ pub fn App() -> impl IntoView { } }); - // Mobile Sidebar Scroll Lock - create_effect(move |_| { - if let Some(doc) = window().document() { - if let Some(body) = doc.body() { - let style = body.style(); - if show_mobile_sidebar.get() { - let _ = style.set_property("overflow", "hidden"); - } else { - let _ = style.remove_property("overflow"); - } - } - } - }); - - // Context Menu Signals - let (cm_visible, set_cm_visible) = create_signal(false); - let (cm_pos, set_cm_pos) = create_signal((0, 0)); - let (cm_target_hash, set_cm_target_hash) = create_signal(String::new()); - - // Delete Confirmation State - let (show_delete_modal, set_show_delete_modal) = create_signal(false); - let (pending_action, set_pending_action) = create_signal(Option::<(String, String)>::None); // (Action, Hash) - // Debug: Last Updated Timestamp let (last_updated, set_last_updated) = create_signal(0u64); @@ -121,15 +99,6 @@ pub fn App() -> impl IntoView { items }); - let sort = move |key: i32| { - if sort_key.get() == key { - set_sort_asc.update(|a| *a = !*a); - } else { - set_sort_key.set(key); - set_sort_asc.set(true); - } - }; - // Add Torrent Logic let (show_modal, set_show_modal) = create_signal(false); let (magnet_link, set_magnet_link) = create_signal(String::new()); @@ -152,7 +121,6 @@ pub fn App() -> impl IntoView { // Connect SSE create_effect(move |_| { spawn_local(async move { - logging::log!("Connecting to SSE..."); let mut es = EventSource::new("/api/events").unwrap(); let mut stream = es.subscribe("message").unwrap(); @@ -160,539 +128,112 @@ pub fn App() -> impl IntoView { match stream.next().await { Some(Ok((_, msg))) => { let data = msg.data().as_string().unwrap(); - match serde_json::from_str::(&data) { - Ok(event) => { - match event { - AppEvent::FullList(list, ts) => { - set_torrents.set(list); - set_last_updated.set(ts); - } - AppEvent::Update(diff) => { - set_torrents.update(|list| { - if let Some(target) = list.iter_mut().find(|t| t.hash == diff.hash) { - if let Some(v) = diff.name { target.name = v; } - if let Some(v) = diff.size { target.size = v; } - if let Some(v) = diff.down_rate { target.down_rate = v; } - if let Some(v) = diff.up_rate { target.up_rate = v; } - if let Some(v) = diff.percent_complete { target.percent_complete = v; } - if let Some(v) = diff.completed { target.completed = v; } - if let Some(v) = diff.eta { target.eta = v; } - if let Some(v) = diff.status { target.status = v; } - if let Some(v) = diff.error_message { target.error_message = v; } - } - }); - } + if let Ok(event) = serde_json::from_str::(&data) { + match event { + AppEvent::FullList(list, ts) => { + set_torrents.set(list); + set_last_updated.set(ts); } - } - Err(e) => { - logging::error!("Failed to parse SSE JSON: {}", e); - } + AppEvent::Update(diff) => { + set_torrents.update(|list| { + if let Some(target) = list.iter_mut().find(|t| t.hash == diff.hash) { + if let Some(v) = diff.name { target.name = v; } + if let Some(v) = diff.size { target.size = v; } + if let Some(v) = diff.down_rate { target.down_rate = v; } + if let Some(v) = diff.up_rate { target.up_rate = v; } + if let Some(v) = diff.percent_complete { target.percent_complete = v; } + if let Some(v) = diff.completed { target.completed = v; } + if let Some(v) = diff.eta { target.eta = v; } + if let Some(v) = diff.status { target.status = v; } + if let Some(v) = diff.error_message { target.error_message = v; } + } + }); + } + } } } - Some(Err(e)) => { - logging::error!("SSE Stream Error: {:?}", e); - } - None => { - logging::warn!("SSE Stream Ended (None received)"); - break; - } + None => break, + _ => {} } } - logging::warn!("SSE Task Exiting"); }); }); - // Formatting Helpers - let format_bytes = |bytes: i64| { - if bytes < 1024 { format!("{} B", bytes) } - else if bytes < 1048576 { format!("{:.1} KB", bytes as f64 / 1024.0) } - else if bytes < 1073741824 { format!("{:.1} MB", bytes as f64 / 1048576.0) } - else { format!("{:.1} GB", bytes as f64 / 1073741824.0) } - }; - - let format_eta = |eta: i64| { - if eta <= 0 || eta > 31536000 { return "∞".to_string(); } - let h = eta / 3600; - let m = (eta % 3600) / 60; - format!("{}h {}m", h, m) - }; - - // Theme Engine - - - let filter_btn_class = move |status: Option| { - 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| { - 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 || { - view! { -
-
- - - -
-

- "VibeTorrent" -

-
- -
"Filters"
- - -
-
-
-
"Storage"
-
-
-
-
- "700 GB used" - "1 TB total" -
-
-
- } - }; - - let theme_option = move |t: Theme, label: &str, color: &str| { - let is_active = theme.get() == t; - let border_class = if is_active { "border-blue-500 ring-1 ring-blue-500/50" } else { "border-transparent hover:border-gray-500/30" }; - let label_owned = label.to_string(); - let color_owned = color.to_string(); - - view! { - - } - }; + // Toolbar Callbacks + let on_add = Callback::new(move |_| set_show_modal.set(true)); + let on_start = Callback::new(move |_| logging::log!("Start all - to be implemented with selection")); + let on_pause = Callback::new(move |_| logging::log!("Pause all - to be implemented with selection")); + let on_delete = Callback::new(move |_| logging::log!("Delete - to be implemented with selection")); + let on_settings = Callback::new(move |_| set_active_tab.set(if active_tab.get() == "settings" { "torrents" } else { "settings" })); view! { -
- // DESKTOP SIDEBAR - - - // MOBILE SIDEBAR -
-
- +
+ +
+ + + {move || if active_tab.get() == "settings" { + view! { +
+

"Settings"

+
+ + + +
+
+ }.into_view() + } else { + view! { }.into_view() + }} + +
- // 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" - } - }} -

-
-
- - - - -
-
- -
- {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) - - - // 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 - - - // Modal (Dark backdrop always) + // Add Torrent Modal (Inlined) -
-
-
-

"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."

-
-
} } diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index 4c7e7cd..e00000a 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -1,4 +1,7 @@ pub mod modal; pub mod context_menu; -pub mod ui; +pub mod toolbar; +pub mod sidebar; +pub mod status_bar; +pub mod torrent_table; diff --git a/frontend/src/components/modal.rs b/frontend/src/components/modal.rs index 05f5746..06dff21 100644 --- a/frontend/src/components/modal.rs +++ b/frontend/src/components/modal.rs @@ -6,7 +6,7 @@ pub fn Modal( children: Children, #[prop(into)] on_confirm: Callback<()>, #[prop(into)] on_cancel: Callback<()>, - #[prop(into)] visible: Signal, + #[prop(into)] is_open: MaybeSignal, #[prop(into, default = "Confirm".to_string())] confirm_text: String, #[prop(into, default = "Cancel".to_string())] cancel_text: String, #[prop(into, default = false)] is_danger: bool, @@ -20,7 +20,7 @@ pub fn Modal( let cancel_text = store_value(cancel_text); view! { - +

{title.get_value()}

diff --git a/frontend/src/components/sidebar.rs b/frontend/src/components/sidebar.rs new file mode 100644 index 0000000..117405d --- /dev/null +++ b/frontend/src/components/sidebar.rs @@ -0,0 +1,52 @@ +use leptos::*; +use thaw::*; +use shared::TorrentStatus; + +#[component] +pub fn Sidebar( + #[prop(into)] active_filter: Signal>, + #[prop(into)] on_filter_change: Callback>, +) -> impl IntoView { + view! { +
+
"Groups"
+
+ + + + + +
+
+ } +} diff --git a/frontend/src/components/status_bar.rs b/frontend/src/components/status_bar.rs new file mode 100644 index 0000000..6025edc --- /dev/null +++ b/frontend/src/components/status_bar.rs @@ -0,0 +1,20 @@ +use leptos::*; +use thaw::*; + +#[component] +pub fn StatusBar() -> impl IntoView { + view! { +
+
+ + "0 KB/s" +
+
+ + "0 KB/s" +
+
+
"Free Space: 700 GB"
+
+ } +} diff --git a/frontend/src/components/toolbar.rs b/frontend/src/components/toolbar.rs new file mode 100644 index 0000000..96f76b4 --- /dev/null +++ b/frontend/src/components/toolbar.rs @@ -0,0 +1,34 @@ +use leptos::*; +use thaw::*; + +#[component] +pub fn Toolbar( + #[prop(into)] on_add: Callback<()>, + #[prop(into)] on_start: Callback<()>, + #[prop(into)] on_pause: Callback<()>, + #[prop(into)] on_delete: Callback<()>, + #[prop(into)] on_settings: Callback<()>, +) -> impl IntoView { + view! { +
+ +
+ + + +
+ + +
+ } +} diff --git a/frontend/src/components/torrent_table.rs b/frontend/src/components/torrent_table.rs new file mode 100644 index 0000000..a7d65f1 --- /dev/null +++ b/frontend/src/components/torrent_table.rs @@ -0,0 +1,51 @@ +use leptos::*; +use thaw::*; +use shared::Torrent; + +#[component] +pub fn TorrentTable( + #[prop(into)] torrents: Signal> +) -> impl IntoView { + view! { +
+ + + + + + + + + + + + + + + + + + + + + + + } + } + /> + +
"Name""Size""Progress""Status""Down""Up""ETA"
{torrent.name}{torrent.size} + + + + {format!("{:?}", torrent.status)} + + {torrent.down_rate}{torrent.up_rate}{torrent.eta}
+
+ } +} diff --git a/frontend/src/components/ui/button.rs b/frontend/src/components/ui/button.rs deleted file mode 100644 index 1b3e5c2..0000000 --- a/frontend/src/components/ui/button.rs +++ /dev/null @@ -1,62 +0,0 @@ -use leptos::*; -use crate::utils::cn; - -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum ButtonVariant { - #[default] - Default, - Destructive, - Outline, - Secondary, - Ghost, - Link, -} - -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum ButtonSize { - #[default] - Default, - Sm, - Lg, - Icon, -} - -#[component] -pub fn Button( - #[prop(into, optional)] variant: ButtonVariant, - #[prop(into, optional)] size: ButtonSize, - #[prop(into, optional)] class: MaybeSignal, - #[prop(into, optional)] on_click: Option>, - children: Children, -) -> impl IntoView { - let variant_classes = match variant { - ButtonVariant::Default => "bg-primary text-primary-foreground hover:bg-primary/90", - ButtonVariant::Destructive => "bg-destructive text-destructive-foreground hover:bg-destructive/90", - ButtonVariant::Outline => "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - ButtonVariant::Secondary => "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ButtonVariant::Ghost => "hover:bg-accent hover:text-accent-foreground", - ButtonVariant::Link => "text-primary underline-offset-4 hover:underline", - }; - - let size_classes = match size { - ButtonSize::Default => "h-10 px-4 py-2", - ButtonSize::Sm => "h-9 rounded-md px-3", - ButtonSize::Lg => "h-11 rounded-md px-8", - ButtonSize::Icon => "h-10 w-10", - }; - - let base_classes = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"; - - view! { - - } -} diff --git a/frontend/src/components/ui/mod.rs b/frontend/src/components/ui/mod.rs deleted file mode 100644 index aa200ca..0000000 --- a/frontend/src/components/ui/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod button;