feat: integrate shadcn/ui, add Button component, and refactor App UI
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -689,6 +689,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
|
"tailwind_fuse",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -2225,6 +2226,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tailwind_fuse"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ca71fb01735fbc6fa13e9390d7a3037dde97053c0b65c0c72c0159cd009d26b"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ futures = "0.3"
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
web-sys = { version = "0.3", features = ["Window", "Storage"] }
|
web-sys = { version = "0.3", features = ["Window", "Storage"] }
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
|
tailwind_fuse = "0.3.2"
|
||||||
|
|||||||
@@ -16,8 +16,150 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@layer base {
|
||||||
--color-gray-900: #111827;
|
|
||||||
--color-gray-800: #1f2937;
|
:root {
|
||||||
--color-gray-700: #374151;
|
--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-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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,21 +14,15 @@
|
|||||||
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
||||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
--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-500: oklch(62.7% 0.265 303.9);
|
||||||
--color-purple-600: oklch(55.8% 0.288 302.321);
|
--color-purple-600: oklch(55.8% 0.288 302.321);
|
||||||
--color-pink-500: oklch(65.6% 0.241 354.308);
|
--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-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-300: oklch(87.2% 0.01 258.338);
|
||||||
--color-gray-400: oklch(70.7% 0.022 261.325);
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||||
--color-gray-500: oklch(55.1% 0.027 264.364);
|
--color-gray-500: oklch(55.1% 0.027 264.364);
|
||||||
--color-gray-600: oklch(44.6% 0.03 256.802);
|
--color-gray-700: oklch(37.3% 0.034 259.733);
|
||||||
--color-gray-700: #374151;
|
--color-gray-800: oklch(27.8% 0.033 256.848);
|
||||||
--color-gray-800: #1f2937;
|
|
||||||
--color-gray-900: #111827;
|
|
||||||
--color-black: #000;
|
--color-black: #000;
|
||||||
--color-white: #fff;
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
@@ -48,19 +42,38 @@
|
|||||||
--text-6xl: 3.75rem;
|
--text-6xl: 3.75rem;
|
||||||
--text-6xl--line-height: 1;
|
--text-6xl--line-height: 1;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--tracking-tight: -0.025em;
|
--tracking-tight: -0.025em;
|
||||||
--tracking-wider: 0.05em;
|
--tracking-wider: 0.05em;
|
||||||
--tracking-widest: 0.1em;
|
--tracking-widest: 0.1em;
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: 0.75rem;
|
--radius-xl: 0.75rem;
|
||||||
--radius-2xl: 1rem;
|
--radius-2xl: 1rem;
|
||||||
--blur-sm: 8px;
|
--blur-sm: 8px;
|
||||||
--blur-md: 12px;
|
|
||||||
--blur-xl: 24px;
|
--blur-xl: 24px;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
--default-mono-font-family: var(--font-mono);
|
--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 {
|
@layer base {
|
||||||
@@ -254,8 +267,8 @@
|
|||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.left-4 {
|
.left-3 {
|
||||||
left: calc(var(--spacing) * 4);
|
left: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.z-10 {
|
.z-10 {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -311,9 +324,6 @@
|
|||||||
.mb-6 {
|
.mb-6 {
|
||||||
margin-bottom: calc(var(--spacing) * 6);
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
.mb-8 {
|
|
||||||
margin-bottom: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.mb-10 {
|
.mb-10 {
|
||||||
margin-bottom: calc(var(--spacing) * 10);
|
margin-bottom: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
@@ -341,6 +351,9 @@
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.inline-flex {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
.h-1\.5 {
|
.h-1\.5 {
|
||||||
height: calc(var(--spacing) * 1.5);
|
height: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
@@ -356,9 +369,15 @@
|
|||||||
.h-6 {
|
.h-6 {
|
||||||
height: calc(var(--spacing) * 6);
|
height: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.h-9 {
|
||||||
|
height: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
.h-10 {
|
.h-10 {
|
||||||
height: calc(var(--spacing) * 10);
|
height: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.h-11 {
|
||||||
|
height: calc(var(--spacing) * 11);
|
||||||
|
}
|
||||||
.h-12 {
|
.h-12 {
|
||||||
height: calc(var(--spacing) * 12);
|
height: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
@@ -468,6 +487,9 @@
|
|||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
.gap-1\.5 {
|
.gap-1\.5 {
|
||||||
gap: calc(var(--spacing) * 1.5);
|
gap: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
@@ -510,6 +532,11 @@
|
|||||||
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.divide-border {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
.truncate {
|
.truncate {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -530,6 +557,9 @@
|
|||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: calc(infinity * 1px);
|
border-radius: calc(infinity * 1px);
|
||||||
}
|
}
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
.rounded-xl {
|
.rounded-xl {
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
}
|
}
|
||||||
@@ -562,15 +592,15 @@
|
|||||||
border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
|
border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.border-blue-500\/30 {
|
.border-border {
|
||||||
border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent);
|
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)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
border-color: color-mix(in oklab, var(--color-blue-500) 30%, transparent);
|
border-color: color-mix(in oklab, var(--color-destructive) 20%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.border-gray-200 {
|
|
||||||
border-color: var(--color-gray-200);
|
|
||||||
}
|
|
||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
border-color: var(--color-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-color: color-mix(in oklab, var(--color-green-500) 20%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.border-red-500\/20 {
|
.border-input {
|
||||||
border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent);
|
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)) {
|
@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-transparent {
|
||||||
border-color: 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-white\/10 {
|
||||||
border-color: color-mix(in srgb, #fff 10%, transparent);
|
border-color: color-mix(in srgb, #fff 10%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -616,18 +643,18 @@
|
|||||||
.bg-\[\#0a0a0c\] {
|
.bg-\[\#0a0a0c\] {
|
||||||
background-color: #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 {
|
.bg-\[\#111116\]\/95 {
|
||||||
background-color: color-mix(in oklab, #111116 95%, transparent);
|
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 {
|
.bg-black {
|
||||||
background-color: var(--color-black);
|
background-color: var(--color-black);
|
||||||
}
|
}
|
||||||
@@ -637,24 +664,6 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-black) 5%, transparent);
|
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 {
|
.bg-blue-500 {
|
||||||
background-color: var(--color-blue-500);
|
background-color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
@@ -664,14 +673,26 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-blue-500) 10%, transparent);
|
background-color: color-mix(in oklab, var(--color-blue-500) 10%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bg-blue-600\/20 {
|
.bg-card {
|
||||||
background-color: color-mix(in srgb, oklch(54.6% 0.245 262.881) 20%, transparent);
|
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)) {
|
@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 {
|
.bg-destructive {
|
||||||
background-color: var(--color-gray-50);
|
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 {
|
.bg-gray-100 {
|
||||||
background-color: var(--color-gray-100);
|
background-color: var(--color-gray-100);
|
||||||
@@ -679,18 +700,6 @@
|
|||||||
.bg-gray-400 {
|
.bg-gray-400 {
|
||||||
background-color: var(--color-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 {
|
.bg-green-500 {
|
||||||
background-color: var(--color-green-500);
|
background-color: var(--color-green-500);
|
||||||
}
|
}
|
||||||
@@ -700,22 +709,37 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-green-500) 10%, transparent);
|
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 {
|
.bg-red-500 {
|
||||||
background-color: var(--color-red-500);
|
background-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
.bg-red-500\/10 {
|
.bg-secondary {
|
||||||
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent);
|
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)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent);
|
background-color: color-mix(in oklab, var(--color-secondary) 50%, transparent);
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-white {
|
|
||||||
background-color: var(--color-white);
|
|
||||||
}
|
|
||||||
.bg-white\/5 {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bg-white\/10 {
|
.bg-white\/10 {
|
||||||
@@ -724,12 +748,6 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
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 {
|
.bg-yellow-500 {
|
||||||
background-color: var(--color-yellow-500);
|
background-color: var(--color-yellow-500);
|
||||||
}
|
}
|
||||||
@@ -751,19 +769,11 @@
|
|||||||
--tw-gradient-from: var(--color-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));
|
--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 {
|
.via-purple-500 {
|
||||||
--tw-gradient-via: var(--color-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-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);
|
--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 {
|
.to-pink-500 {
|
||||||
--tw-gradient-to: var(--color-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));
|
--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 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.p-2\.5 {
|
.p-3 {
|
||||||
padding: calc(var(--spacing) * 2.5);
|
padding: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
@@ -809,12 +819,12 @@
|
|||||||
.px-4 {
|
.px-4 {
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.px-5 {
|
|
||||||
padding-inline: calc(var(--spacing) * 5);
|
|
||||||
}
|
|
||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.px-8 {
|
||||||
|
padding-inline: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.py-1 {
|
.py-1 {
|
||||||
padding-block: calc(var(--spacing) * 1);
|
padding-block: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -824,12 +834,6 @@
|
|||||||
.py-2\.5 {
|
.py-2\.5 {
|
||||||
padding-block: calc(var(--spacing) * 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 {
|
.py-4 {
|
||||||
padding-block: calc(var(--spacing) * 4);
|
padding-block: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -848,8 +852,8 @@
|
|||||||
.pb-24 {
|
.pb-24 {
|
||||||
padding-bottom: calc(var(--spacing) * 24);
|
padding-bottom: calc(var(--spacing) * 24);
|
||||||
}
|
}
|
||||||
.pl-12 {
|
.pl-10 {
|
||||||
padding-left: calc(var(--spacing) * 12);
|
padding-left: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -901,6 +905,10 @@
|
|||||||
--tw-font-weight: var(--font-weight-medium);
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
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 {
|
.tracking-tight {
|
||||||
--tw-tracking: var(--tracking-tight);
|
--tw-tracking: var(--tracking-tight);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
@@ -919,26 +927,38 @@
|
|||||||
.text-blue-500 {
|
.text-blue-500 {
|
||||||
color: var(--color-blue-500);
|
color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
.text-gray-200 {
|
.text-card-foreground {
|
||||||
color: var(--color-gray-200);
|
color: var(--color-card-foreground);
|
||||||
}
|
}
|
||||||
.text-gray-400 {
|
.text-destructive {
|
||||||
color: var(--color-gray-400);
|
color: var(--color-destructive);
|
||||||
|
}
|
||||||
|
.text-destructive-foreground {
|
||||||
|
color: var(--color-destructive-foreground);
|
||||||
|
}
|
||||||
|
.text-foreground {
|
||||||
|
color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
.text-gray-500 {
|
.text-gray-500 {
|
||||||
color: var(--color-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 {
|
.text-green-500 {
|
||||||
color: var(--color-green-500);
|
color: var(--color-green-500);
|
||||||
}
|
}
|
||||||
.text-red-400 {
|
.text-muted-foreground {
|
||||||
color: var(--color-red-400);
|
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 {
|
.text-red-500 {
|
||||||
color: var(--color-red-500);
|
color: var(--color-red-500);
|
||||||
@@ -946,6 +966,9 @@
|
|||||||
.text-red-600 {
|
.text-red-600 {
|
||||||
color: var(--color-red-600);
|
color: var(--color-red-600);
|
||||||
}
|
}
|
||||||
|
.text-secondary-foreground {
|
||||||
|
color: var(--color-secondary-foreground);
|
||||||
|
}
|
||||||
.text-transparent {
|
.text-transparent {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
@@ -958,6 +981,9 @@
|
|||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
.underline-offset-4 {
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
.opacity-5 {
|
.opacity-5 {
|
||||||
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));
|
--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);
|
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 {
|
.ring-1 {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--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);
|
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 {
|
.shadow-blue-500\/30 {
|
||||||
--tw-shadow-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent);
|
--tw-shadow-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@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);
|
--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 {
|
.ring-blue-500\/50 {
|
||||||
--tw-ring-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 50%, transparent);
|
--tw-ring-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 50%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
--tw-ring-color: color-mix(in oklab, var(--color-blue-500) 50%, transparent);
|
--tw-ring-color: color-mix(in oklab, var(--color-blue-500) 50%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ring-white\/5 {
|
.ring-offset-background {
|
||||||
--tw-ring-color: color-mix(in srgb, #fff 5%, transparent);
|
--tw-ring-offset-color: var(--color-background);
|
||||||
@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,);
|
|
||||||
}
|
}
|
||||||
.backdrop-blur-sm {
|
.backdrop-blur-sm {
|
||||||
--tw-backdrop-blur: blur(var(--blur-sm));
|
--tw-backdrop-blur: blur(var(--blur-sm));
|
||||||
@@ -1074,61 +1084,9 @@
|
|||||||
--tw-duration: 500ms;
|
--tw-duration: 500ms;
|
||||||
transition-duration: 500ms;
|
transition-duration: 500ms;
|
||||||
}
|
}
|
||||||
.selection\:bg-blue-500\/20 {
|
.placeholder\:text-muted-foreground {
|
||||||
& *::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 {
|
&::placeholder {
|
||||||
color: var(--color-gray-600);
|
color: var(--color-muted-foreground);
|
||||||
}
|
|
||||||
}
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:border-gray-500\/30 {
|
.hover\:border-gray-500\/30 {
|
||||||
@@ -1141,36 +1099,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:border-white\/10 {
|
.hover\:bg-accent {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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)) {
|
@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 {
|
&:hover {
|
||||||
@media (hover: 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 {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
background-color: var(--color-gray-100);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-gray-900 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-gray-900);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.hover\:bg-red-500\/20 {
|
.hover\:bg-red-500\/20 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -1182,13 +1146,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-red-600 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-red-600);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:bg-red-900\/20 {
|
.hover\:bg-red-900\/20 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -1199,12 +1156,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-white\/5 {
|
.hover\:bg-secondary\/80 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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)) {
|
@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 {
|
&:hover {
|
||||||
@media (hover: 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 {
|
&:hover {
|
||||||
@media (hover: 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\)\] {
|
.focus\:border-ring {
|
||||||
&: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 {
|
&:focus {
|
||||||
border-color: var(--color-blue-500);
|
border-color: var(--color-ring);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.focus\:ring-1 {
|
.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);
|
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 {
|
&:focus {
|
||||||
--tw-ring-color: var(--color-blue-500);
|
--tw-ring-color: var(--color-ring);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.focus\:outline-none {
|
.focus\:outline-none {
|
||||||
@@ -1295,12 +1233,27 @@
|
|||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.active\:scale-95 {
|
.focus-visible\:ring-2 {
|
||||||
&:active {
|
&:focus-visible {
|
||||||
--tw-scale-x: 95%;
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
--tw-scale-y: 95%;
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
--tw-scale-z: 95%;
|
}
|
||||||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
}
|
||||||
|
.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\] {
|
.active\:scale-\[0\.98\] {
|
||||||
@@ -1308,6 +1261,16 @@
|
|||||||
scale: 0.98;
|
scale: 0.98;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled\:pointer-events-none {
|
||||||
|
&:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled\:opacity-50 {
|
||||||
|
&:disabled {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.sm\:grid-cols-3 {
|
.sm\:grid-cols-3 {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -1358,9 +1321,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.md\:rounded-2xl {
|
.md\:rounded-lg {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
border-radius: var(--radius-2xl);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.md\:pt-6 {
|
.md\:pt-6 {
|
||||||
@@ -1387,6 +1350,75 @@
|
|||||||
touch-action: manipulation;
|
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 {
|
@property --tw-translate-x {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -1592,21 +1624,6 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
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 {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@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 {
|
*, ::before, ::after, ::backdrop {
|
||||||
@@ -1656,9 +1673,6 @@
|
|||||||
--tw-backdrop-saturate: initial;
|
--tw-backdrop-saturate: initial;
|
||||||
--tw-backdrop-sepia: initial;
|
--tw-backdrop-sepia: initial;
|
||||||
--tw-duration: initial;
|
--tw-duration: initial;
|
||||||
--tw-scale-x: 1;
|
|
||||||
--tw-scale-y: 1;
|
|
||||||
--tw-scale-z: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use leptos::*;
|
|||||||
use shared::{Torrent, AppEvent, TorrentStatus, Theme};
|
use shared::{Torrent, AppEvent, TorrentStatus, Theme};
|
||||||
use crate::components::context_menu::ContextMenu;
|
use crate::components::context_menu::ContextMenu;
|
||||||
use crate::components::modal::Modal;
|
use crate::components::modal::Modal;
|
||||||
|
use crate::components::ui::button::{Button, ButtonVariant};
|
||||||
use gloo_net::eventsource::futures::EventSource;
|
use gloo_net::eventsource::futures::EventSource;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
|
||||||
@@ -26,12 +27,34 @@ pub fn App() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Persist Theme
|
// Persist Theme
|
||||||
|
// Persist Theme & Apply CSS Variables
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
let val = match theme.get() {
|
let val = match theme.get() {
|
||||||
Theme::Midnight => "Midnight",
|
Theme::Midnight => "Midnight",
|
||||||
Theme::Light => "Light",
|
Theme::Light => "Light",
|
||||||
Theme::Amoled => "Amoled",
|
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() {
|
if let Some(storage) = window().local_storage().ok().flatten() {
|
||||||
let _ = storage.set_item("vibetorrent_theme", val);
|
let _ = storage.set_item("vibetorrent_theme", val);
|
||||||
}
|
}
|
||||||
@@ -195,62 +218,32 @@ pub fn App() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Theme Engine
|
// 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<TorrentStatus>| {
|
let filter_btn_class = move |status: Option<TorrentStatus>| {
|
||||||
let (_base_bg, _, _, _, text_sec, hover, _) = get_theme_classes();
|
crate::utils::cn(format!(
|
||||||
let base = "block px-4 py-2 rounded-xl transition-all duration-200 text-left w-full flex items-center gap-3 border";
|
"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 {}",
|
||||||
let active = filter_status.get() == status;
|
if filter_status.get() == status {
|
||||||
if active {
|
"bg-primary/10 text-primary border-primary/20"
|
||||||
format!("{} bg-blue-600/20 text-blue-500 border-blue-500/30 font-medium", base)
|
|
||||||
} else {
|
} else {
|
||||||
format!("{} {} {} border-transparent hover:text-gray-300", base, hover, text_sec)
|
"border-transparent text-muted-foreground hover:text-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
}
|
}
|
||||||
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
let tab_btn_class = move |tab: &str| {
|
let tab_btn_class = move |tab: &str| {
|
||||||
let active = active_tab.get() == tab;
|
crate::utils::cn(format!(
|
||||||
let base = "flex flex-col items-center justify-center p-2 flex-1 transition-colors relative";
|
"flex flex-col items-center justify-center p-2 flex-1 transition-colors relative {}",
|
||||||
if active {
|
if active_tab.get() == tab {
|
||||||
format!("{} text-blue-500", base)
|
"text-primary"
|
||||||
} else {
|
} else {
|
||||||
"flex flex-col items-center justify-center p-2 flex-1 transition-colors relative text-gray-400 hover:text-gray-300".to_string()
|
"text-muted-foreground hover:text-foreground"
|
||||||
}
|
}
|
||||||
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sidebar Content Logic
|
// Sidebar Content Logic
|
||||||
let sidebar_content = move || {
|
let sidebar_content = move || {
|
||||||
let (_, _, _, _text_pri, text_sec, _, border) = get_theme_classes();
|
|
||||||
view! {
|
view! {
|
||||||
<div class="mb-10 px-2 flex items-center gap-3">
|
<div class="mb-10 px-2 flex items-center gap-3">
|
||||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-lg shadow-blue-500/30">
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-lg shadow-blue-500/30">
|
||||||
@@ -263,7 +256,7 @@ pub fn App() -> impl IntoView {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={format!("text-xs font-bold uppercase tracking-widest mb-4 px-2 {}", text_sec)}>"Filters"</div>
|
<div class="text-xs font-bold uppercase tracking-widest mb-4 px-2 text-muted-foreground">"Filters"</div>
|
||||||
<nav class="space-y-2 flex-1">
|
<nav class="space-y-2 flex-1">
|
||||||
<button class={move || filter_btn_class(None)} on:click=move |_| { set_filter_status.set(None); set_show_mobile_sidebar.set(false); }>
|
<button class={move || filter_btn_class(None)} on:click=move |_| { set_filter_status.set(None); set_show_mobile_sidebar.set(false); }>
|
||||||
<span class="w-2 h-2 rounded-full bg-gray-400"></span>
|
<span class="w-2 h-2 rounded-full bg-gray-400"></span>
|
||||||
@@ -287,14 +280,14 @@ pub fn App() -> impl IntoView {
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class={format!("mt-auto pt-6 border-t {}", border)}>
|
<div class="mt-auto pt-6 border-t border-border">
|
||||||
<div class={format!("rounded-xl p-4 border relative overflow-hidden {}", border)}>
|
<div class="rounded-xl p-4 border border-border relative overflow-hidden bg-card">
|
||||||
<div class={format!("absolute inset-0 opacity-5 {}", if theme.get() == Theme::Light { "bg-black" } else { "bg-white" })}></div>
|
<div class="absolute inset-0 opacity-5 bg-foreground"></div>
|
||||||
<div class={format!("text-xs mb-2 z-10 relative {}", text_sec)}>"Storage"</div>
|
<div class="text-xs mb-2 z-10 relative text-muted-foreground">"Storage"</div>
|
||||||
<div class="w-full bg-gray-500/20 rounded-full h-1.5 mb-2 overflow-hidden z-10 relative">
|
<div class="w-full bg-secondary/50 rounded-full h-1.5 mb-2 overflow-hidden z-10 relative">
|
||||||
<div class="bg-gradient-to-r from-blue-500 to-purple-500 w-[70%] h-full rounded-full"></div>
|
<div class="bg-gradient-to-r from-blue-500 to-purple-500 w-[70%] h-full rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class={format!("flex justify-between text-xs z-10 relative {}", text_sec)}>
|
<div class="flex justify-between text-xs z-10 relative text-muted-foreground">
|
||||||
<span>"700 GB used"</span>
|
<span>"700 GB used"</span>
|
||||||
<span>"1 TB total"</span>
|
<span>"1 TB total"</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,13 +326,9 @@ pub fn App() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
{move || {
|
<div class="min-h-screen font-sans flex flex-col md:flex-row overflow-hidden transition-colors duration-300 bg-background text-foreground">
|
||||||
let (main_bg, sidebar_bg, card_bg, text_pri, text_sec, hover, border) = get_theme_classes();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class={format!("min-h-screen font-sans flex flex-col md:flex-row overflow-hidden transition-colors duration-300 {}", main_bg)}>
|
|
||||||
// DESKTOP SIDEBAR
|
// DESKTOP SIDEBAR
|
||||||
<aside class={format!("hidden md:flex flex-col w-72 border-r p-6 z-20 h-screen {}", sidebar_bg)}>
|
<aside class="hidden md:flex flex-col w-72 border-r border-border p-6 z-20 h-screen bg-card/50 backdrop-blur-xl">
|
||||||
{sidebar_content}
|
{sidebar_content}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -349,14 +338,14 @@ pub fn App() -> impl IntoView {
|
|||||||
on:click=move |_| ()
|
on:click=move |_| ()
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity cursor-default"
|
class="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity cursor-default"
|
||||||
on:click=move |_| set_show_mobile_sidebar.set(false)
|
on:click=move |_| set_show_mobile_sidebar.set(false)
|
||||||
></div>
|
></div>
|
||||||
<aside
|
<aside
|
||||||
class={format!("relative w-80 max-w-[85vw] h-full shadow-2xl p-6 flex flex-col animate-in slide-in-from-left duration-300 border-r {}", sidebar_bg)}
|
class="relative w-80 max-w-[85vw] h-full shadow-2xl p-6 flex flex-col animate-in slide-in-from-left duration-300 border-r border-border bg-card"
|
||||||
on:click=move |e: web_sys::MouseEvent| e.stop_propagation()
|
on:click=move |e: web_sys::MouseEvent| e.stop_propagation()
|
||||||
>
|
>
|
||||||
<button class={format!("absolute top-4 right-4 p-2 hover:opacity-80 {}", text_sec)} on:click=move |_| set_show_mobile_sidebar.set(false)>
|
<button class="absolute top-4 right-4 p-2 hover:opacity-80 text-muted-foreground" on:click=move |_| set_show_mobile_sidebar.set(false)>
|
||||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
</button>
|
</button>
|
||||||
{sidebar_content}
|
{sidebar_content}
|
||||||
@@ -365,12 +354,12 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
// MAIN CONTENT
|
// MAIN CONTENT
|
||||||
<main class="flex-1 h-screen overflow-y-auto overflow-x-hidden relative pb-24 md:pb-0">
|
<main class="flex-1 h-screen overflow-y-auto overflow-x-hidden relative pb-24 md:pb-0">
|
||||||
<header class={format!("fixed top-0 left-0 right-0 md:sticky md:top-0 z-40 border-b px-6 py-4 flex justify-between items-center transition-colors duration-300 {}", sidebar_bg)}>
|
<header class="fixed top-0 left-0 right-0 md:sticky md:top-0 z-40 border-b border-border px-6 py-4 flex justify-between items-center transition-colors duration-300 bg-background/80 backdrop-blur-xl">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button class={format!("md:hidden p-1 -ml-2 hover:opacity-80 {}", text_sec)} on:click=move |_| set_show_mobile_sidebar.set(true)>
|
<button class="md:hidden p-1 -ml-2 hover:opacity-80 text-muted-foreground" on:click=move |_| set_show_mobile_sidebar.set(true)>
|
||||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<h2 class={format!("text-xl font-bold flex items-center gap-2 {}", text_pri)}>
|
<h2 class="text-xl font-bold flex items-center gap-2 text-foreground">
|
||||||
{move || if active_tab.get() == "settings" { "Settings" } else if active_tab.get() == "dashboard" { "Dashboard" } else {
|
{move || if active_tab.get() == "settings" { "Settings" } else if active_tab.get() == "dashboard" { "Dashboard" } else {
|
||||||
match filter_status.get() {
|
match filter_status.get() {
|
||||||
None => "All Torrents",
|
None => "All Torrents",
|
||||||
@@ -384,7 +373,7 @@ pub fn App() -> impl IntoView {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class={format!("hidden md:block text-xs font-mono {}", text_sec)}>
|
<div class="hidden md:block text-xs font-mono text-muted-foreground">
|
||||||
"Server Time: "
|
"Server Time: "
|
||||||
{move || {
|
{move || {
|
||||||
let ts = last_updated.get();
|
let ts = last_updated.get();
|
||||||
@@ -398,25 +387,25 @@ pub fn App() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
class="px-5 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl hover:shadow-lg hover:shadow-blue-500/30 hover:scale-105 active:scale-95 transition-all text-sm font-bold text-white flex items-center gap-2"
|
class="gap-2"
|
||||||
on:click=move |_| set_show_modal.set(true)
|
on_click=Callback::from(move |_| set_show_modal.set(true))
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /></svg>
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /></svg>
|
||||||
<span class="hidden md:inline">"Add Torrent"</span>
|
<span class="hidden md:inline">"Add Torrent"</span>
|
||||||
<span class="md:hidden">"Add"</span>
|
<span class="md:hidden">"Add"</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
class={format!("hidden md:flex p-2.5 rounded-xl hover:bg-white/5 active:scale-95 transition-all text-gray-400 hover:text-white border border-transparent hover:border-white/10 {}", if active_tab.get() == "settings" { "bg-blue-500/10 text-blue-500 border-blue-500/20" } else { "" })}
|
variant=ButtonVariant::Ghost
|
||||||
on:click=move |_| set_active_tab.set(if active_tab.get() == "settings" { "torrents" } else { "settings" })
|
class={move || if active_tab.get() == "settings" { "text-primary bg-primary/10 border-primary/20" } else { "text-muted-foreground" }}
|
||||||
title="Settings"
|
on_click=Callback::from(move |_| set_active_tab.set(if active_tab.get() == "settings" { "torrents" } else { "settings" }))
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -425,40 +414,40 @@ pub fn App() -> impl IntoView {
|
|||||||
view! {
|
view! {
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class={format!("text-lg font-bold mb-4 {}", text_pri)}>"Appearance"</h3>
|
<h3 class="text-lg font-bold mb-4 text-foreground">"Appearance"</h3>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
{theme_option(Theme::Midnight, "Midnight", "bg-[#0a0a0c] border border-gray-700")}
|
{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::Light, "Light", "bg-gray-100 border border-gray-300")}
|
||||||
{theme_option(Theme::Amoled, "Amoled", "bg-black border border-gray-800")}
|
{theme_option(Theme::Amoled, "Amoled", "bg-black border border-gray-800")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={format!("p-6 rounded-2xl border {}", card_bg)}>
|
<div class="p-6 rounded-2xl border border-border bg-card shadow-sm">
|
||||||
<h3 class={format!("text-lg font-bold mb-2 {}", text_pri)}>"About VibeTorrent"</h3>
|
<h3 class="text-lg font-bold mb-2 text-foreground">"About VibeTorrent"</h3>
|
||||||
<p class={format!("text-sm {}", text_sec)}>"Version 3.0.0 (Rust + WebAssembly)"</p>
|
<p class="text-sm text-muted-foreground">"Version 3.0.0 (Rust + WebAssembly)"</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
}.into_view()
|
||||||
} else if active_tab.get() == "dashboard" {
|
} else if active_tab.get() == "dashboard" {
|
||||||
view! {
|
view! {
|
||||||
<div class="text-center py-20 opacity-50">"Dashboard Charts Coming Soon..."</div>
|
<div class="text-center py-20 opacity-50 text-muted-foreground">"Dashboard Charts Coming Soon..."</div>
|
||||||
}.into_view()
|
}.into_view()
|
||||||
} else {
|
} else {
|
||||||
view! {
|
view! {
|
||||||
// Torrent List (Desktop)
|
// Torrent List (Desktop)
|
||||||
<div class={format!("hidden md:block rounded-2xl border shadow-sm overflow-hidden {}", card_bg)}>
|
<div class="hidden md:block rounded-2xl border border-border bg-card shadow-sm overflow-hidden">
|
||||||
<table class="w-full text-left table-fixed">
|
<table class="w-full text-left table-fixed">
|
||||||
<thead class={format!("uppercase text-xs font-bold tracking-wider {}", text_sec)}>
|
<thead class="uppercase text-xs font-bold tracking-wider text-muted-foreground bg-muted/50 border-b border-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80" on:click=move |_| sort(0)>"Name"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors" on:click=move |_| sort(0)>"Name"</th>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80 w-28 text-right whitespace-nowrap" on:click=move |_| sort(1)>"Size"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors w-28 text-right whitespace-nowrap" on:click=move |_| sort(1)>"Size"</th>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80 w-36" on:click=move |_| sort(2)>"Progress"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors w-36" on:click=move |_| sort(2)>"Progress"</th>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80 w-28 text-right whitespace-nowrap" on:click=move |_| sort(3)>"Down"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors w-28 text-right whitespace-nowrap" on:click=move |_| sort(3)>"Down"</th>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80 w-28 text-right whitespace-nowrap" on:click=move |_| sort(4)>"Up"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors w-28 text-right whitespace-nowrap" on:click=move |_| sort(4)>"Up"</th>
|
||||||
<th class="px-6 py-4 cursor-pointer hover:opacity-80 w-28 text-right whitespace-nowrap" on:click=move |_| sort(5)>"ETA"</th>
|
<th class="px-6 py-4 cursor-pointer hover:text-foreground transition-colors w-28 text-right whitespace-nowrap" on:click=move |_| sort(5)>"ETA"</th>
|
||||||
<th class="px-6 py-4 text-center w-28">"Status"</th>
|
<th class="px-6 py-4 text-center w-28">"Status"</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class={format!("divide-y {}", border)}>
|
<tbody class="divide-y divide-border">
|
||||||
<For
|
<For
|
||||||
each=move || processed_torrents.get()
|
each=move || processed_torrents.get()
|
||||||
key=|t| format!("{}-{}-{:?}-{}-{}-{}-{}", t.hash, t.name, t.status, t.down_rate, t.up_rate, t.percent_complete, t.error_message)
|
key=|t| format!("{}-{}-{:?}-{}-{}-{}-{}", t.hash, t.name, t.status, t.down_rate, t.up_rate, t.percent_complete, t.error_message)
|
||||||
@@ -467,8 +456,8 @@ pub fn App() -> impl IntoView {
|
|||||||
TorrentStatus::Downloading => "text-blue-500 bg-blue-500/10 border-blue-500/20",
|
TorrentStatus::Downloading => "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::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::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",
|
TorrentStatus::Error => "text-destructive bg-destructive/10 border-destructive/20",
|
||||||
_ => "text-gray-400 bg-gray-500/10"
|
_ => "text-muted-foreground bg-muted"
|
||||||
};
|
};
|
||||||
let status_text = format!("{:?}", torrent.status);
|
let status_text = format!("{:?}", torrent.status);
|
||||||
let error_msg = torrent.error_message.clone();
|
let error_msg = torrent.error_message.clone();
|
||||||
@@ -476,7 +465,7 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<tr
|
<tr
|
||||||
class={format!("transition-colors group {}", hover)}
|
class="transition-colors group hover:bg-muted/50"
|
||||||
on:contextmenu=move |e: web_sys::MouseEvent| {
|
on:contextmenu=move |e: web_sys::MouseEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
set_cm_pos.set((e.client_x(), e.client_y()));
|
set_cm_pos.set((e.client_x(), e.client_y()));
|
||||||
@@ -485,42 +474,42 @@ pub fn App() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<td class="px-6 py-4 max-w-sm">
|
<td class="px-6 py-4 max-w-sm">
|
||||||
<div class={format!("font-medium truncate transition-colors {}", text_pri)} title={torrent.name.clone()}>
|
<div class="font-medium truncate transition-colors text-foreground" title={torrent.name.clone()}>
|
||||||
{torrent.name}
|
{torrent.name}
|
||||||
</div>
|
</div>
|
||||||
<Show when=move || !error_msg.is_empty() fallback=|| ()>
|
<Show when=move || !error_msg.is_empty() fallback=|| ()>
|
||||||
<div class="text-xs text-red-500 mt-1">{error_msg_view.clone()}</div>
|
<div class="text-xs text-destructive mt-1">{error_msg_view.clone()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
<td class={format!("px-6 py-4 text-sm font-mono text-right whitespace-nowrap {}", text_sec)}>{format_bytes(torrent.size)}</td>
|
<td class="px-6 py-4 text-sm font-mono text-right whitespace-nowrap text-muted-foreground">{format_bytes(torrent.size)}</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<div class={format!("flex justify-between text-xs {}", text_sec)}>
|
<div class="flex justify-between text-xs text-muted-foreground">
|
||||||
<span>{format!("{:.1}%", torrent.percent_complete)}</span>
|
<span>{format!("{:.1}%", torrent.percent_complete)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-500/20 rounded-full h-1.5 overflow-hidden">
|
<div class="w-full bg-secondary rounded-full h-1.5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="bg-blue-500 h-full rounded-full transition-all duration-500"
|
class="bg-primary h-full rounded-full transition-all duration-500"
|
||||||
style=format!("width: {}%", torrent.percent_complete)
|
style=format!("width: {}%", torrent.percent_complete)
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class={format!("px-6 py-4 font-mono text-xs text-right whitespace-nowrap {}", text_sec)}>
|
<td class="px-6 py-4 font-mono text-xs text-right whitespace-nowrap text-muted-foreground">
|
||||||
{if torrent.down_rate > 0 {
|
{if torrent.down_rate > 0 {
|
||||||
view! { <span class="text-blue-500">{format_bytes(torrent.down_rate)} "/s"</span> }.into_view()
|
view! { <span class="text-blue-500">{format_bytes(torrent.down_rate)} "/s"</span> }.into_view()
|
||||||
} else {
|
} else {
|
||||||
view! { <span class="text-gray-600">"-"</span> }.into_view()
|
view! { <span class="text-muted-foreground/50">"-"</span> }.into_view()
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td class={format!("px-6 py-4 font-mono text-xs text-right whitespace-nowrap {}", text_sec)}>
|
<td class="px-6 py-4 font-mono text-xs text-right whitespace-nowrap text-muted-foreground">
|
||||||
{if torrent.up_rate > 0 {
|
{if torrent.up_rate > 0 {
|
||||||
view! { <span class="text-green-500">{format_bytes(torrent.up_rate)} "/s"</span> }.into_view()
|
view! { <span class="text-green-500">{format_bytes(torrent.up_rate)} "/s"</span> }.into_view()
|
||||||
} else {
|
} else {
|
||||||
view! { <span class="text-gray-600">"-"</span> }.into_view()
|
view! { <span class="text-muted-foreground/50">"-"</span> }.into_view()
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td class={format!("px-6 py-4 text-xs font-mono text-right whitespace-nowrap {}", text_sec)}>
|
<td class="px-6 py-4 text-xs font-mono text-right whitespace-nowrap text-muted-foreground">
|
||||||
{format_eta(torrent.eta)}
|
{format_eta(torrent.eta)}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
@@ -546,26 +535,26 @@ pub fn App() -> impl IntoView {
|
|||||||
TorrentStatus::Downloading => "text-blue-500",
|
TorrentStatus::Downloading => "text-blue-500",
|
||||||
TorrentStatus::Seeding => "text-green-500",
|
TorrentStatus::Seeding => "text-green-500",
|
||||||
TorrentStatus::Paused => "text-yellow-500",
|
TorrentStatus::Paused => "text-yellow-500",
|
||||||
TorrentStatus::Error => "text-red-500",
|
TorrentStatus::Error => "text-destructive",
|
||||||
_ => "text-gray-400"
|
_ => "text-muted-foreground"
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class={format!("rounded-2xl p-4 border shadow-sm active:scale-[0.98] transition-transform {}", card_bg)}>
|
<div class="rounded-2xl p-4 border border-border shadow-sm active:scale-[0.98] transition-transform bg-card">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div class={format!("font-medium line-clamp-2 pr-4 {}", text_pri)}>{torrent.name}</div>
|
<div class="font-medium line-clamp-2 pr-4 text-foreground">{torrent.name}</div>
|
||||||
<div class={format!("text-xs font-bold {}", status_color)}>
|
<div class={format!("text-xs font-bold {}", status_color)}>
|
||||||
{format!("{:?}", torrent.status)}
|
{format!("{:?}", torrent.status)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class={format!("flex justify-between text-xs mb-1 {}", text_sec)}>
|
<div class="flex justify-between text-xs mb-1 text-muted-foreground">
|
||||||
<span>{format_bytes(torrent.size)}</span>
|
<span>{format_bytes(torrent.size)}</span>
|
||||||
<span>{format!("{:.1}%", torrent.percent_complete)}</span>
|
<span>{format!("{:.1}%", torrent.percent_complete)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-500/20 rounded-full h-1.5 overflow-hidden">
|
<div class="w-full bg-secondary rounded-full h-1.5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="bg-blue-500 h-full rounded-full transition-all duration-500"
|
class="bg-primary h-full rounded-full transition-all duration-500"
|
||||||
style=format!("width: {}%", torrent.percent_complete)
|
style=format!("width: {}%", torrent.percent_complete)
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -575,14 +564,14 @@ pub fn App() -> impl IntoView {
|
|||||||
<span class="text-blue-500">"↓ " {format_bytes(torrent.down_rate)} "/s"</span>
|
<span class="text-blue-500">"↓ " {format_bytes(torrent.down_rate)} "/s"</span>
|
||||||
<span class="text-green-500">"↑ " {format_bytes(torrent.up_rate)} "/s"</span>
|
<span class="text-green-500">"↑ " {format_bytes(torrent.up_rate)} "/s"</span>
|
||||||
</div>
|
</div>
|
||||||
<div class={text_sec}>{format_eta(torrent.eta)}</div>
|
<div class="text-muted-foreground">{format_eta(torrent.eta)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Show when=move || processed_torrents.get().is_empty() fallback=|| ()>
|
<Show when=move || processed_torrents.get().is_empty() fallback=|| ()>
|
||||||
<div class={format!("p-12 text-center mt-10 {}", text_sec)}>
|
<div class="p-12 text-center mt-10 text-muted-foreground">
|
||||||
<div class="mb-4 text-6xl opacity-20">"📭"</div>
|
<div class="mb-4 text-6xl opacity-20">"📭"</div>
|
||||||
"No torrents found."
|
"No torrents found."
|
||||||
</div>
|
</div>
|
||||||
@@ -594,7 +583,7 @@ pub fn App() -> impl IntoView {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
// MOBILE BOTTOM NAV
|
// MOBILE BOTTOM NAV
|
||||||
<nav class={format!("md:hidden fixed bottom-0 inset-x-0 backdrop-blur-xl border-t pb-safe z-30 flex justify-between items-center px-6 py-2 {}", sidebar_bg)}>
|
<nav class="md:hidden fixed bottom-0 inset-x-0 backdrop-blur-xl border-t border-border pb-safe z-30 flex justify-between items-center px-6 py-2 bg-background/80">
|
||||||
<button class={move || tab_btn_class("torrents")} on:click=move |_| set_active_tab.set("torrents")>
|
<button class={move || tab_btn_class("torrents")} on:click=move |_| set_active_tab.set("torrents")>
|
||||||
<svg class="w-6 h-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
<svg class="w-6 h-6 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
||||||
<span class="text-[10px] font-medium">"List"</span>
|
<span class="text-[10px] font-medium">"List"</span>
|
||||||
@@ -611,31 +600,31 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
// Modal (Dark backdrop always)
|
// Modal (Dark backdrop always)
|
||||||
<Show when=move || show_modal.get() fallback=|| ()>
|
<Show when=move || show_modal.get() fallback=|| ()>
|
||||||
<div class="fixed inset-0 bg-black/80 backdrop-blur-md flex items-end md:items-center justify-center z-50 animate-in fade-in duration-200 sm:p-4">
|
<div class="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-end md:items-center justify-center z-50 animate-in fade-in duration-200 sm:p-4">
|
||||||
<div class="bg-[#16161c] p-6 rounded-t-2xl md:rounded-2xl w-full max-w-lg shadow-2xl border border-white/10 ring-1 ring-white/5 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
<div class="bg-card p-6 rounded-t-2xl md:rounded-lg w-full max-w-lg shadow-xl border border-border ring-0 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h3 class="text-xl font-bold text-white">"Add New Torrent"</h3>
|
<h3 class="text-xl font-bold text-card-foreground">"Add New Torrent"</h3>
|
||||||
<button on:click=move |_| set_show_modal.set(false) class="p-1 hover:bg-white/10 rounded-full transition-colors">
|
<button on:click=move |_| set_show_modal.set(false) class="p-1 hover:bg-accent rounded-full transition-colors text-muted-foreground">
|
||||||
<svg class="w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative mb-6">
|
<div class="relative mb-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full bg-black/30 border border-white/10 rounded-xl p-4 pl-12 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none transition-all placeholder:text-gray-600"
|
class="w-full bg-input border border-input rounded-md p-3 pl-10 text-foreground focus:border-ring focus:ring-1 focus:ring-ring focus:outline-none transition-all placeholder:text-muted-foreground"
|
||||||
placeholder="Paste Magnet Link or URL"
|
placeholder="Paste Magnet Link or URL"
|
||||||
on:input=move |ev| set_magnet_link.set(event_target_value(&ev))
|
on:input=move |ev| set_magnet_link.set(event_target_value(&ev))
|
||||||
prop:value=magnet_link
|
prop:value=magnet_link
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">
|
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button class="flex-1 px-4 py-3.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl hover:shadow-[0_0_20px_rgba(59,130,246,0.3)] transition-all font-bold text-white shadow-lg active:scale-95" on:click=add_torrent>
|
<Button class="flex-1" on_click=Callback::from(move |_| add_torrent())>
|
||||||
"Add Download"
|
"Add Download"
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -701,11 +690,9 @@ pub fn App() -> impl IntoView {
|
|||||||
>
|
>
|
||||||
<p>"Are you definitely sure you want to delete this torrent?"</p>
|
<p>"Are you definitely sure you want to delete this torrent?"</p>
|
||||||
<Show when=move || pending_action.get().map(|(a, _)| a == "delete_with_data").unwrap_or(false)>
|
<Show when=move || pending_action.get().map(|(a, _)| a == "delete_with_data").unwrap_or(false)>
|
||||||
<p class="mt-2 text-red-400 font-bold">"⚠️ This will also permanently delete the downloaded files from the disk."</p>
|
<p class="mt-2 text-destructive font-bold">"⚠️ This will also permanently delete the downloaded files from the disk."</p>
|
||||||
</Show>
|
</Show>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
pub mod context_menu;
|
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
|
pub mod context_menu;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
|||||||
@@ -21,25 +21,26 @@ pub fn Modal(
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || visible.get() fallback=|| ()>
|
<Show when=move || visible.get() fallback=|| ()>
|
||||||
<div class="fixed inset-0 bg-black/80 backdrop-blur-md flex items-end md:items-center justify-center z-[200] animate-in fade-in duration-200 sm:p-4">
|
<div class="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-end md:items-center justify-center z-[200] animate-in fade-in duration-200 sm:p-4">
|
||||||
<div class="bg-[#16161c] p-6 rounded-t-2xl md:rounded-2xl w-full max-w-sm shadow-2xl border border-white/10 ring-1 ring-white/5 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
<div class="bg-card p-6 rounded-t-2xl md:rounded-lg w-full max-w-sm shadow-xl border border-border ring-0 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
||||||
<h3 class="text-xl font-bold text-white mb-4">{title.get_value()}</h3>
|
<h3 class="text-lg font-semibold text-card-foreground mb-4">{title.get_value()}</h3>
|
||||||
|
|
||||||
<div class="text-gray-400 mb-8">
|
<div class="text-muted-foreground mb-6 text-sm">
|
||||||
{child_view.with_value(|c| c.clone())}
|
{child_view.with_value(|c| c.clone())}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
class="flex-1 px-4 py-3 bg-white/5 hover:bg-white/10 rounded-xl transition-all font-medium text-white"
|
class="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 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
||||||
on:click=move |_| on_cancel.with_value(|cb| cb.call(()))
|
on:click=move |_| on_cancel.with_value(|cb| cb.call(()))
|
||||||
>
|
>
|
||||||
{cancel_text.get_value()}
|
{cancel_text.get_value()}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class=format!("flex-1 px-4 py-3 rounded-xl transition-all font-bold text-white shadow-lg {}",
|
class=move || crate::utils::cn(format!("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 h-10 px-4 py-2 {}",
|
||||||
if is_danger { "bg-red-500 hover:bg-red-600 shadow-red-500/20" } else { "bg-blue-500 hover:bg-blue-600 shadow-blue-500/20" }
|
if is_danger { "bg-destructive text-destructive-foreground hover:bg-destructive/90" }
|
||||||
)
|
else { "bg-primary text-primary-foreground hover:bg-primary/90" }
|
||||||
|
))
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
logging::log!("Modal: Confirm clicked");
|
logging::log!("Modal: Confirm clicked");
|
||||||
on_confirm.with_value(|cb| cb.call(()))
|
on_confirm.with_value(|cb| cb.call(()))
|
||||||
|
|||||||
62
frontend/src/components/ui/button.rs
Normal file
62
frontend/src/components/ui/button.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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: String,
|
||||||
|
#[prop(into, optional)] on_click: Option<Callback<web_sys::MouseEvent>>,
|
||||||
|
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! {
|
||||||
|
<button
|
||||||
|
class=cn(format!("{} {} {} {}", base_classes, variant_classes, size_classes, class))
|
||||||
|
on:click=move |e| {
|
||||||
|
if let Some(cb) = on_click {
|
||||||
|
cb.call(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children()}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/src/components/ui/mod.rs
Normal file
1
frontend/src/components/ui/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod button;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
// mod models; // Removed
|
// mod models; // Removed
|
||||||
mod components;
|
mod components;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|||||||
5
frontend/src/utils/mod.rs
Normal file
5
frontend/src/utils/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use tailwind_fuse::merge::tw_merge;
|
||||||
|
|
||||||
|
pub fn cn(classes: impl AsRef<str>) -> String {
|
||||||
|
tw_merge(classes.as_ref())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user