feat: implement mobile PWA card design, refine toolbar and status bar

This commit is contained in:
spinline
2026-02-01 13:46:31 +03:00
parent e932fa1e39
commit 8f7af0d1f8
7 changed files with 736 additions and 386 deletions

View File

@@ -7,13 +7,6 @@
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace; "Courier New", monospace;
--color-red-400: oklch(70.4% 0.191 22.216);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-900: oklch(39.6% 0.141 25.723);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-gray-500: oklch(55.1% 0.027 264.364);
--color-black: #000; --color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
@@ -25,18 +18,16 @@
--text-sm--line-height: calc(1.25 / 0.875); --text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem; --text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125); --text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem; --font-weight-normal: 400;
--text-xl--line-height: calc(1.75 / 1.25);
--font-weight-medium: 500; --font-weight-medium: 500;
--font-weight-semibold: 600; --font-weight-semibold: 600;
--font-weight-bold: 700; --font-weight-bold: 700;
--tracking-wider: 0.05em; --tracking-wider: 0.05em;
--leading-tight: 1.25;
--radius-md: 0.375rem; --radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem; --radius-2xl: 1rem;
--blur-sm: 8px; --blur-sm: 8px;
--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);
@@ -514,6 +505,95 @@
} }
} }
} }
.dropdown {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-block;
position-area: var(--anchor-v, bottom) var(--anchor-h, span-right);
& > *:not(:has(~ [class*="dropdown-content"])):focus {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
.dropdown-content {
position: absolute;
}
&.dropdown-close .dropdown-content, &:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) .dropdown-content, &.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible) ~ .dropdown-content {
display: none;
transform-origin: top;
opacity: 0%;
scale: 95%;
}
&[popover], .dropdown-content {
z-index: 999;
@media (prefers-reduced-motion: no-preference) {
animation: dropdown 0.2s;
transition-property: opacity, scale, display;
transition-behavior: allow-discrete;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
}
@starting-style {
&[popover], .dropdown-content {
scale: 95%;
opacity: 0;
}
}
&:not(.dropdown-close) {
&.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within {
> [tabindex]:first-child {
pointer-events: none;
}
.dropdown-content {
opacity: 100%;
scale: 100%;
}
}
&.dropdown-hover:hover {
.dropdown-content {
opacity: 100%;
scale: 100%;
}
}
}
&:is(details) {
summary {
&::-webkit-details-marker {
display: none;
}
}
}
&:where([popover]) {
background: #0000;
}
&[popover] {
position: fixed;
color: inherit;
@supports not (position-area: bottom) {
margin: auto;
&.dropdown-close, &.dropdown-open:not(:popover-open) {
display: none;
transform-origin: top;
opacity: 0%;
scale: 95%;
}
&::backdrop {
background-color: color-mix(in oklab, #000 30%, #0000);
}
}
&.dropdown-close, &:not(.dropdown-open, :popover-open) {
display: none;
transform-origin: top;
opacity: 0%;
scale: 95%;
}
}
}
}
.btn { .btn {
:where(&) { :where(&) {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
@@ -940,6 +1020,20 @@
} }
} }
} }
.navbar {
@layer daisyui.l1.l2.l3 {
display: flex;
width: 100%;
align-items: center;
padding: 0.5rem;
min-height: 4rem;
}
:where(&) {
@layer daisyui.l1.l2 {
position: relative;
}
}
}
.drawer { .drawer {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
position: relative; position: relative;
@@ -948,6 +1042,55 @@
grid-auto-columns: max-content auto; grid-auto-columns: max-content auto;
} }
} }
.card {
@layer daisyui.l1.l2.l3 {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--radius-box);
outline-width: 2px;
transition: outline 0.2s ease-in-out;
outline: 0 solid #0000;
outline-offset: 2px;
&:focus {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
&:focus-visible {
outline-color: currentColor;
}
:where(figure:first-child) {
overflow: hidden;
border-start-start-radius: inherit;
border-start-end-radius: inherit;
border-end-start-radius: unset;
border-end-end-radius: unset;
}
:where(figure:last-child) {
overflow: hidden;
border-start-start-radius: unset;
border-start-end-radius: unset;
border-end-start-radius: inherit;
border-end-end-radius: inherit;
}
figure {
display: flex;
align-items: center;
justify-content: center;
}
&:has(> input:is(input[type="checkbox"], input[type="radio"])) {
cursor: pointer;
user-select: none;
}
&:has(> :checked) {
outline: 2px solid currentColor;
}
}
}
.progress { .progress {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
position: relative; position: relative;
@@ -1010,6 +1153,44 @@
.inset-0 { .inset-0 {
inset: calc(var(--spacing) * 0); inset: calc(var(--spacing) * 0);
} }
.dropdown-end {
@layer daisyui.l1.l2 {
--anchor-h: span-left;
:where(.dropdown-content) {
inset-inline-end: calc(0.25rem * 0);
translate: 0 0;
[dir="rtl"] & {
translate: 0 0;
}
}
&.dropdown-left {
--anchor-h: left;
--anchor-v: span-top;
.dropdown-content {
top: auto;
bottom: calc(0.25rem * 0);
}
}
&.dropdown-right {
--anchor-h: right;
--anchor-v: span-top;
.dropdown-content {
top: auto;
bottom: calc(0.25rem * 0);
}
}
}
}
.dropdown-top {
@layer daisyui.l1.l2 {
--anchor-v: top;
.dropdown-content {
top: auto;
bottom: 100%;
transform-origin: bottom;
}
}
}
.modal-backdrop { .modal-backdrop {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
grid-column-start: 1; grid-column-start: 1;
@@ -1027,6 +1208,9 @@
.z-40 { .z-40 {
z-index: 40; z-index: 40;
} }
.z-\[1\] {
z-index: 1;
}
.z-\[100\] { .z-\[100\] {
z-index: 100; z-index: 100;
} }
@@ -1061,6 +1245,36 @@
min-width: calc(0.25rem * 0); min-width: calc(0.25rem * 0);
} }
} }
.divider {
@layer daisyui.l1.l2.l3 {
display: flex;
height: calc(0.25rem * 4);
flex-direction: row;
align-items: center;
align-self: stretch;
white-space: nowrap;
margin: var(--divider-m, 1rem 0);
--divider-color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
--divider-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
}
&:before, &:after {
content: "";
height: calc(0.25rem * 0.5);
width: 100%;
flex-grow: 1;
background-color: var(--divider-color);
}
@media print {
&:before, &:after {
border: 0.5px solid;
}
}
&:not(:empty) {
gap: calc(0.25rem * 4);
}
}
}
.filter { .filter {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
display: flex; display: flex;
@@ -1118,8 +1332,8 @@
} }
} }
} }
.my-1 { .my-0 {
margin-block: calc(var(--spacing) * 1); margin-block: calc(var(--spacing) * 0);
} }
.label { .label {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
@@ -1160,15 +1374,6 @@
} }
} }
} }
.join-item {
&:where(*:not(:first-child, :disabled, [disabled], .btn-disabled)) {
margin-inline-start: calc(var(--border, 1px) * -1);
margin-block-start: 0;
}
&:where(*:is(:disabled, [disabled], .btn-disabled)) {
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
}
}
.modal-action { .modal-action {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
margin-top: calc(0.25rem * 6); margin-top: calc(0.25rem * 6);
@@ -1183,9 +1388,6 @@
.mt-auto { .mt-auto {
margin-top: auto; margin-top: auto;
} }
.mb-1 {
margin-bottom: calc(var(--spacing) * 1);
}
.mb-2 { .mb-2 {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
} }
@@ -1248,71 +1450,50 @@
height: var(--size); height: var(--size);
} }
} }
.join { .navbar-end {
@layer daisyui.l1.l2.l3 {
display: inline-flex; display: inline-flex;
align-items: stretch; align-items: center;
--join-ss: 0; width: 50%;
--join-se: 0; justify-content: flex-end;
--join-es: 0;
--join-ee: 0;
:where(.join-item) {
border-start-start-radius: var(--join-ss, 0);
border-start-end-radius: var(--join-se, 0);
border-end-start-radius: var(--join-es, 0);
border-end-end-radius: var(--join-ee, 0);
* {
--join-ss: var(--radius-field);
--join-se: var(--radius-field);
--join-es: var(--radius-field);
--join-ee: var(--radius-field);
} }
} }
> .join-item:where(:first-child) { .navbar-start {
--join-ss: var(--radius-field); @layer daisyui.l1.l2.l3 {
--join-se: 0; display: inline-flex;
--join-es: var(--radius-field); align-items: center;
--join-ee: 0; width: 50%;
} justify-content: flex-start;
:first-child:not(:last-child) {
:where(.join-item) {
--join-ss: var(--radius-field);
--join-se: 0;
--join-es: var(--radius-field);
--join-ee: 0;
} }
} }
> .join-item:where(:last-child) { .card-body {
--join-ss: 0; @layer daisyui.l1.l2.l3 {
--join-se: var(--radius-field); display: flex;
--join-es: 0; flex: auto;
--join-ee: var(--radius-field); flex-direction: column;
} gap: calc(0.25rem * 2);
:last-child:not(:first-child) { padding: var(--card-p, 1.5rem);
:where(.join-item) { font-size: var(--card-fs, 0.875rem);
--join-ss: 0; :where(p) {
--join-se: var(--radius-field); flex-grow: 1;
--join-es: 0;
--join-ee: var(--radius-field);
} }
} }
> .join-item:where(:only-child) {
--join-ss: var(--radius-field);
--join-se: var(--radius-field);
--join-es: var(--radius-field);
--join-ee: var(--radius-field);
}
:only-child {
:where(.join-item) {
--join-ss: var(--radius-field);
--join-se: var(--radius-field);
--join-es: var(--radius-field);
--join-ee: var(--radius-field);
}
} }
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
} }
.flex { .flex {
display: flex; display: flex;
} }
.grid {
display: grid;
}
.hidden {
display: none;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
@@ -1346,6 +1527,9 @@
height: var(--size); height: var(--size);
} }
} }
.h-1\.5 {
height: calc(var(--spacing) * 1.5);
}
.h-4 { .h-4 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
@@ -1385,15 +1569,15 @@
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
} }
.w-8 {
width: calc(var(--spacing) * 8);
}
.w-24 { .w-24 {
width: calc(var(--spacing) * 24); width: calc(var(--spacing) * 24);
} }
.w-48 { .w-48 {
width: calc(var(--spacing) * 48); width: calc(var(--spacing) * 48);
} }
.w-52 {
width: calc(var(--spacing) * 52);
}
.w-64 { .w-64 {
width: calc(var(--spacing) * 64); width: calc(var(--spacing) * 64);
} }
@@ -1421,12 +1605,15 @@
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
.cursor-context-menu {
cursor: context-menu;
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -1436,6 +1623,12 @@
.items-end { .items-end {
align-items: flex-end; align-items: flex-end;
} }
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
@@ -1472,6 +1665,9 @@
.overflow-x-auto { .overflow-x-auto {
overflow-x: auto; overflow-x: auto;
} }
.overflow-y-auto {
overflow-y: auto;
}
.rounded-box { .rounded-box {
border-radius: var(--radius-box); border-radius: var(--radius-box);
} }
@@ -1481,12 +1677,6 @@
.rounded-md { .rounded-md {
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.rounded-none {
border-radius: 0;
}
.rounded-xl {
border-radius: var(--radius-xl);
}
.rounded-t-2xl { .rounded-t-2xl {
border-top-left-radius: var(--radius-2xl); border-top-left-radius: var(--radius-2xl);
border-top-right-radius: var(--radius-2xl); border-top-right-radius: var(--radius-2xl);
@@ -1507,6 +1697,10 @@
border-bottom-style: var(--tw-border-style); border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px; border-bottom-width: 1px;
} }
.border-l {
border-left-style: var(--tw-border-style);
border-left-width: 1px;
}
.badge-ghost { .badge-ghost {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
border-color: var(--color-base-200); border-color: var(--color-base-200);
@@ -1515,21 +1709,38 @@
background-image: none; background-image: none;
} }
} }
.badge-soft {
@layer daisyui.l1.l2 {
color: var(--badge-color, var(--color-base-content));
background-color: var(--badge-color, var(--color-base-content));
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) );
}
border-color: var(--badge-color, var(--color-base-content));
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) );
}
background-image: none;
}
}
.border-base-200 { .border-base-200 {
border-color: var(--color-base-200); border-color: var(--color-base-200);
} }
.border-base-200\/50 {
border-color: var(--color-base-200);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-base-200) 50%, transparent);
}
}
.border-base-300 { .border-base-300 {
border-color: var(--color-base-300); border-color: var(--color-base-300);
} }
.border-white\/10 { .border-white\/5 {
border-color: color-mix(in srgb, #fff 10%, transparent); border-color: color-mix(in srgb, #fff 5%, 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); border-color: color-mix(in oklab, var(--color-white) 5%, transparent);
} }
} }
.bg-\[\#111116\]\/95 {
background-color: color-mix(in oklab, #111116 95%, transparent);
}
.bg-base-100 { .bg-base-100 {
background-color: var(--color-base-100); background-color: var(--color-base-100);
} }
@@ -1539,10 +1750,10 @@
.bg-primary { .bg-primary {
background-color: var(--color-primary); background-color: var(--color-primary);
} }
.bg-white\/10 { .bg-primary\/10 {
background-color: color-mix(in srgb, #fff 10%, transparent); background-color: var(--color-primary);
@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) 10%, transparent); background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
} }
} }
.loading-spinner { .loading-spinner {
@@ -1553,29 +1764,41 @@
.stroke-current { .stroke-current {
stroke: currentcolor; stroke: currentcolor;
} }
.checkbox-xs {
@layer daisyui.l1.l2 {
padding: 0.125rem;
--size: calc(var(--size-selector, 0.25rem) * 4);
}
}
.p-0 { .p-0 {
padding: calc(var(--spacing) * 0); padding: calc(var(--spacing) * 0);
} }
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-3 {
padding: calc(var(--spacing) * 3);
}
.p-4 { .p-4 {
padding: calc(var(--spacing) * 4); padding: calc(var(--spacing) * 4);
} }
.p-6 { .p-6 {
padding: calc(var(--spacing) * 6); padding: calc(var(--spacing) * 6);
} }
.table-xs { .menu-title {
@layer daisyui.l1.l2.l3 {
padding-inline: calc(0.25rem * 3);
padding-block: calc(0.25rem * 2);
color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
}
font-size: 0.875rem;
font-weight: 600;
}
}
.table-sm {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
:not(thead, tfoot) tr { :not(thead, tfoot) tr {
font-size: 0.6875rem; font-size: 0.75rem;
} }
:where(th, td) { :where(th, td) {
padding-inline: calc(0.25rem * 2); padding-inline: calc(0.25rem * 3);
padding-block: calc(0.25rem * 1); padding-block: calc(0.25rem * 2);
} }
} }
} }
@@ -1586,26 +1809,33 @@
padding-inline: calc(0.25rem * 2.5 - var(--border)); padding-inline: calc(0.25rem * 2.5 - var(--border));
} }
} }
.px-3 { .badge-xs {
padding-inline: calc(var(--spacing) * 3); @layer daisyui.l1.l2 {
--size: calc(var(--size-selector, 0.25rem) * 4);
font-size: 0.625rem;
padding-inline: calc(0.25rem * 2 - var(--border));
}
} }
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.py-1 { .py-1\.5 {
padding-block: calc(var(--spacing) * 1); padding-block: calc(var(--spacing) * 1.5);
} }
.py-2 { .py-2 {
padding-block: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 2);
} }
.py-2\.5 {
padding-block: calc(var(--spacing) * 2.5);
}
.py-4 { .py-4 {
padding-block: calc(var(--spacing) * 4); padding-block: calc(var(--spacing) * 4);
} }
.text-left { .pt-1 {
text-align: left; padding-top: calc(var(--spacing) * 1);
}
.pb-20 {
padding-bottom: calc(var(--spacing) * 20);
}
.text-center {
text-align: center;
} }
.text-right { .text-right {
text-align: right; text-align: right;
@@ -1621,20 +1851,23 @@
font-size: var(--text-sm); font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height)); line-height: var(--tw-leading, var(--text-sm--line-height));
} }
.text-xl {
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
.text-xs { .text-xs {
font-size: var(--text-xs); font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height)); line-height: var(--tw-leading, var(--text-xs--line-height));
} }
.text-\[9px\] {
font-size: 9px;
}
.text-\[10px\] { .text-\[10px\] {
font-size: 10px; font-size: 10px;
} }
.text-\[11px\] { .text-\[11px\] {
font-size: 11px; font-size: 11px;
} }
.leading-tight {
--tw-leading: var(--leading-tight);
line-height: var(--leading-tight);
}
.font-bold { .font-bold {
--tw-font-weight: var(--font-weight-bold); --tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
@@ -1643,6 +1876,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-normal {
--tw-font-weight: var(--font-weight-normal);
font-weight: var(--font-weight-normal);
}
.font-semibold { .font-semibold {
--tw-font-weight: var(--font-weight-semibold); --tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
@@ -1673,6 +1910,12 @@
color: color-mix(in oklab, var(--color-base-content) 50%, transparent); color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
} }
} }
.text-base-content\/60 {
color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
}
}
.text-base-content\/70 { .text-base-content\/70 {
color: var(--color-base-content); color: var(--color-base-content);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -1682,32 +1925,17 @@
.text-error { .text-error {
color: var(--color-error); color: var(--color-error);
} }
.text-gray-500 {
color: var(--color-gray-500);
}
.text-green-500 {
color: var(--color-green-500);
}
.text-primary { .text-primary {
color: var(--color-primary); color: var(--color-primary);
} }
.text-red-500 {
color: var(--color-red-500);
}
.text-red-600 {
color: var(--color-red-600);
}
.text-success { .text-success {
color: var(--color-success); color: var(--color-success);
} }
.text-warning { .text-warning {
color: var(--color-warning); color: var(--color-warning);
} }
.text-white { .capitalize {
color: var(--color-white); text-transform: capitalize;
}
.text-yellow-500 {
color: var(--color-yellow-500);
} }
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
@@ -1715,14 +1943,24 @@
.opacity-0 { .opacity-0 {
opacity: 0%; opacity: 0%;
} }
.opacity-10 {
opacity: 10%;
}
.opacity-60 {
opacity: 60%;
}
.opacity-70 { .opacity-70 {
opacity: 70%; opacity: 70%;
} }
.opacity-80 { .opacity-80 {
opacity: 80%; opacity: 80%;
} }
.shadow-2xl { .shadow {
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-xl { .shadow-xl {
@@ -1733,6 +1971,13 @@
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --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); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.ring-2 {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-primary {
--tw-ring-color: var(--color-primary);
}
.btn-ghost { .btn-ghost {
@layer daisyui.l1 { @layer daisyui.l1 {
&:not(.btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn)) { &:not(.btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn)) {
@@ -1765,11 +2010,6 @@
-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,); -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-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-xl {
--tw-backdrop-blur: blur(var(--blur-xl));
-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,);
}
.transition-all { .transition-all {
transition-property: all; transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -1792,26 +2032,6 @@
--tw-duration: 300ms; --tw-duration: 300ms;
transition-duration: 300ms; transition-duration: 300ms;
} }
.btn-outline {
@layer daisyui.l1 {
&:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) {
--btn-shadow: "";
--btn-bg: #0000;
--btn-fg: var(--btn-color);
--btn-border: var(--btn-color);
--btn-noise: none;
}
@media (hover: none) {
&:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover {
--btn-shadow: "";
--btn-bg: #0000;
--btn-fg: var(--btn-color);
--btn-border: var(--btn-color);
--btn-noise: none;
}
}
}
}
.btn-sm { .btn-sm {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
--fontsize: 0.75rem; --fontsize: 0.75rem;
@@ -1826,6 +2046,30 @@
--size: calc(var(--size-field, 0.25rem) * 6); --size: calc(var(--size-field, 0.25rem) * 6);
} }
} }
.badge-error {
@layer daisyui.l1.l2 {
--badge-color: var(--color-error);
--badge-fg: var(--color-error-content);
}
}
.badge-primary {
@layer daisyui.l1.l2 {
--badge-color: var(--color-primary);
--badge-fg: var(--color-primary-content);
}
}
.badge-success {
@layer daisyui.l1.l2 {
--badge-color: var(--color-success);
--badge-fg: var(--color-success-content);
}
}
.badge-warning {
@layer daisyui.l1.l2 {
--badge-color: var(--color-warning);
--badge-fg: var(--color-warning-content);
}
}
.btn-primary { .btn-primary {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
--btn-color: var(--color-primary); --btn-color: var(--color-primary);
@@ -1857,6 +2101,16 @@
} }
} }
} }
.hover\:bg-error\/10 {
&:hover {
@media (hover: hover) {
background-color: var(--color-error);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-error) 10%, transparent);
}
}
}
}
.hover\:bg-primary\/90 { .hover\:bg-primary\/90 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -1867,36 +2121,6 @@
} }
} }
} }
.hover\:bg-red-500\/20 {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 20%, transparent);
}
}
}
}
.hover\:bg-red-900\/20 {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, oklch(39.6% 0.141 25.723) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-900) 20%, transparent);
}
}
}
}
.hover\:bg-white\/10 {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, #fff 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
}
}
}
}
.hover\:text-primary { .hover\:text-primary {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -1904,13 +2128,6 @@
} }
} }
} }
.hover\:text-red-400 {
&:hover {
@media (hover: hover) {
color: var(--color-red-400);
}
}
}
.focus-visible\:ring-2 { .focus-visible\:ring-2 {
&:focus-visible { &:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
@@ -1929,6 +2146,31 @@
outline-style: none; outline-style: none;
} }
} }
.active\:scale-\[0\.99\] {
&:active {
scale: 0.99;
}
}
.active\:bg-error {
&:active {
background-color: var(--color-error);
}
}
.active\:bg-primary {
&:active {
background-color: var(--color-primary);
}
}
.active\:text-error-content {
&:active {
color: var(--color-error-content);
}
}
.active\:text-primary-content {
&:active {
color: var(--color-primary-content);
}
}
.disabled\:pointer-events-none { .disabled\:pointer-events-none {
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
@@ -1963,6 +2205,16 @@
padding: calc(var(--spacing) * 4); padding: calc(var(--spacing) * 4);
} }
} }
.md\:block {
@media (width >= 48rem) {
display: block;
}
}
.md\:hidden {
@media (width >= 48rem) {
display: none;
}
}
.md\:items-center { .md\:items-center {
@media (width >= 48rem) { @media (width >= 48rem) {
align-items: center; align-items: center;
@@ -1973,11 +2225,6 @@
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
} }
} }
.md\:p-6 {
@media (width >= 48rem) {
padding: calc(var(--spacing) * 6);
}
}
.lg\:drawer-open { .lg\:drawer-open {
@media (width >= 64rem) { @media (width >= 64rem) {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
@@ -2317,6 +2564,10 @@
inherits: false; inherits: false;
initial-value: solid; initial-value: solid;
} }
@property --tw-leading {
syntax: "*";
inherits: false;
}
@property --tw-font-weight { @property --tw-font-weight {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@@ -2493,6 +2744,7 @@
--tw-skew-y: initial; --tw-skew-y: initial;
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
--tw-border-style: solid; --tw-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial; --tw-font-weight: initial;
--tw-tracking: initial; --tw-tracking: initial;
--tw-shadow: 0 0 #0000; --tw-shadow: 0 0 #0000;

View File

@@ -17,7 +17,7 @@ pub fn App() -> impl IntoView {
// Toolbar at the top // Toolbar at the top
<Toolbar /> <Toolbar />
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden p-4 md:p-6 space-y-6"> <main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden space-y-6">
<Router> <Router>
<Routes> <Routes>
<Route path="/" view=move || view! { <TorrentTable /> } /> <Route path="/" view=move || view! { <TorrentTable /> } />

View File

@@ -68,38 +68,44 @@ pub fn ContextMenu(
view! { view! {
<div <div
node_ref=target node_ref=target
class="fixed z-[100] bg-[#111116]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl py-2 min-w-[200px] animate-in fade-in zoom-in-95 duration-100" class="fixed z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
style=format!("left: {}px; top: {}px", position.0, position.1) style=format!("left: {}px; top: {}px", position.0, position.1)
on:contextmenu=move |e| e.prevent_default() on:contextmenu=move |e| e.prevent_default()
> >
<div class="px-3 py-1 text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">"Actions"</div> <ul class="menu bg-base-200 text-base-content rounded-box shadow-xl border border-white/5 p-2 gap-1">
<li class="menu-title px-4 py-1.5 text-xs opacity-60 uppercase tracking-wider font-bold">"Actions"</li>
<li>
<button <button
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white" class="gap-3 active:bg-primary active:text-primary-content"
on:click={ on:click={
let handle_action = handle_action.clone(); let handle_action = handle_action.clone();
move |_| handle_action("start") move |_| handle_action("start")
} }
> >
<svg class="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <svg class="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
"Resume" "Resume"
</button> </button>
</li>
<li>
<button <button
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white" class="gap-3 active:bg-primary active:text-primary-content"
on:click={ on:click={
let handle_action = handle_action.clone(); let handle_action = handle_action.clone();
move |_| handle_action("stop") move |_| handle_action("stop")
} }
> >
<svg class="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <svg class="w-4 h-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
"Pause" "Pause"
</button> </button>
</li>
<div class="h-px bg-white/10 my-1"></div> <div class="divider my-0 h-px p-0 opacity-10"></div>
<li>
<button <button
class="w-full text-left px-4 py-2.5 hover:bg-red-500/20 text-red-500 hover:text-red-400 flex items-center gap-3 transition-colors" class="gap-3 text-error hover:bg-error/10 active:bg-error active:text-error-content"
on:click={ on:click={
let handle_action = handle_action.clone(); let handle_action = handle_action.clone();
move |_| handle_action("delete") move |_| handle_action("delete")
@@ -108,9 +114,11 @@ pub fn ContextMenu(
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
"Delete" "Delete"
</button> </button>
</li>
<li>
<button <button
class="w-full text-left px-4 py-2.5 hover:bg-red-900/20 text-red-600 hover:text-red-400 flex items-center gap-3 transition-colors text-xs" class="gap-3 text-error hover:bg-error/10 active:bg-error active:text-error-content text-xs"
on:click={ on:click={
let handle_action = handle_action.clone(); let handle_action = handle_action.clone();
move |_| handle_action("delete_with_data") move |_| handle_action("delete_with_data")
@@ -119,6 +127,8 @@ pub fn ContextMenu(
<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="M13 10V3L4 14h7v7l9-11h-7z" /></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="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
<span>"Delete with Data"</span> <span>"Delete with Data"</span>
</button> </button>
</li>
</ul>
</div> </div>
}.into_view() }.into_view()
} }

View File

@@ -19,10 +19,10 @@ pub fn Sidebar() -> impl IntoView {
}; };
view! { view! {
<div class="w-64 h-full flex flex-col"> <div class="w-64 h-full flex flex-col bg-base-200 border-r border-base-300">
<div class="p-4"> <div class="p-2">
<h2 class="text-xl font-bold px-4 mb-2 text-primary">"Filters"</h2>
<ul class="menu w-full rounded-box gap-1"> <ul class="menu w-full rounded-box gap-1">
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
<li> <li>
<a class={move || filter_class(crate::store::FilterStatus::All)} on:click=move |_| set_filter(crate::store::FilterStatus::All)> <a class={move || filter_class(crate::store::FilterStatus::All)} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
@@ -71,9 +71,9 @@ pub fn Sidebar() -> impl IntoView {
</ul> </ul>
</div> </div>
<div class="mt-auto p-4 border-t border-base-300"> <div class="mt-auto p-2 border-t border-base-300">
<h3 class="text-xs font-bold text-base-content/50 uppercase mb-2 px-4">"Trackers"</h3>
<ul class="menu w-full rounded-box gap-1 text-sm"> <ul class="menu w-full rounded-box gap-1 text-sm">
<li class="menu-title text-base-content/50 uppercase font-bold px-4">"Trackers"</li>
<li><a>"All Trackers"</a></li> <li><a>"All Trackers"</a></li>
<li><a>"Error"</a></li> <li><a>"Error"</a></li>
</ul> </ul>

View File

@@ -23,6 +23,34 @@ pub fn StatusBar() -> impl IntoView {
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</button> </button>
<div class="dropdown dropdown-top dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square" title="Change Theme">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072c0 .657.66 1.175 1.312 1.133 3.421-.22 6.187 2.546 6.187 5.965 0 1.595-.572 3.064-1.524 4.195" />
</svg>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52 mb-2 border border-base-300">
{
let themes = vec!["light", "dark", "cupcake", "dracula", "cyberpunk", "emerald", "luxury", "nord"];
themes.into_iter().map(|theme| {
view! {
<li>
<button
class="text-xs capitalize"
on:click=move |_| {
let doc = web_sys::window().unwrap().document().unwrap();
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
}
>
{theme}
</button>
</li>
}
}).collect::<Vec<_>>()
}
</ul>
</div>
<button class="btn btn-ghost btn-xs btn-square" title="Settings"> <button class="btn btn-ghost btn-xs btn-square" title="Settings">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.212 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.212 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />

View File

@@ -5,51 +5,29 @@ pub fn Toolbar() -> impl IntoView {
let (show_add_modal, set_show_add_modal) = create_signal(false); let (show_add_modal, set_show_add_modal) = create_signal(false);
view! { view! {
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-4"> <div class="navbar min-h-14 h-14 bg-base-100 p-0">
<div class="navbar-start gap-4 px-4">
<label for="my-drawer" class="btn btn-square btn-ghost lg:hidden drawer-button"> <label for="my-drawer" class="btn btn-square btn-ghost lg:hidden drawer-button">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</label> </label>
<div class="join">
<button class="join-item btn btn-sm btn-outline gap-2" title="Open Torrent"> <div class="flex gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
"Open"
</button>
<button <button
class="join-item btn btn-sm btn-outline gap-2" class="btn btn-sm btn-primary gap-2 font-normal"
title="Magnet Link" title="Add Magnet Link"
on:click=move |_| set_show_add_modal.set(true) on:click=move |_| set_show_add_modal.set(true)
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
"URL" "Add Torrent"
</button> </button>
</div> </div>
<div class="join">
<button class="join-item btn btn-sm btn-ghost" title="Start">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-success">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
</button>
<button class="join-item btn btn-sm btn-ghost" title="Pause">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-warning">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
</button>
</div> </div>
<div class="join"> <div class="navbar-end gap-2 px-4">
<button class="join-item btn btn-sm btn-ghost text-error" title="Remove">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
<div class="ml-auto flex items-center gap-2">
<input type="text" placeholder="Filter..." class="input input-sm input-bordered w-full max-w-xs" /> <input type="text" placeholder="Filter..." class="input input-sm input-bordered w-full max-w-xs" />
</div> </div>

View File

@@ -106,14 +106,14 @@ pub fn TorrentTable() -> impl IntoView {
} }
}; };
let (selected_hash, set_selected_hash) = create_signal(Option::<String>::None);
let (menu_visible, set_menu_visible) = create_signal(false); let (menu_visible, set_menu_visible) = create_signal(false);
let (menu_position, set_menu_position) = create_signal((0, 0)); let (menu_position, set_menu_position) = create_signal((0, 0));
let (active_hash, set_active_hash) = create_signal(String::new());
let handle_context_menu = move |e: web_sys::MouseEvent, hash: String| { let handle_context_menu = move |e: web_sys::MouseEvent, hash: String| {
e.prevent_default(); e.prevent_default();
set_menu_position.set((e.client_x(), e.client_y())); set_menu_position.set((e.client_x(), e.client_y()));
set_active_hash.set(hash); set_selected_hash.set(Some(hash)); // Select on right click too
set_menu_visible.set(true); set_menu_visible.set(true);
}; };
@@ -152,14 +152,10 @@ pub fn TorrentTable() -> impl IntoView {
view! { view! {
<div class="overflow-x-auto h-full bg-base-100 relative"> // Added relative for positioning context if needed, though menu is fixed <div class="overflow-x-auto h-full bg-base-100 relative"> // Added relative for positioning context if needed, though menu is fixed
<table class="table table-xs table-pin-rows w-full max-w-full whitespace-nowrap"> <div class="hidden md:block h-full overflow-x-auto">
<table class="table table-sm table-pin-rows w-full max-w-full whitespace-nowrap">
<thead> <thead>
<tr class="bg-base-200 text-base-content/70"> <tr class="text-xs uppercase text-base-content/60 border-b border-base-200">
<th class="w-8">
<label>
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
</label>
</th>
<th class="cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Name)> <th class="cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Name)>
<div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div> <div class="flex items-center">"Name" {move || sort_arrow(SortColumn::Name)}</div>
</th> </th>
@@ -194,21 +190,32 @@ pub fn TorrentTable() -> impl IntoView {
shared::TorrentStatus::Error => "text-error", shared::TorrentStatus::Error => "text-error",
_ => "text-base-content/50" _ => "text-base-content/50"
}; };
let t_hash = t.hash.clone(); // Clone for closure using it in handler let t_hash = t.hash.clone();
let t_hash_click = t.hash.clone();
let is_selected_fn = move || {
selected_hash.get() == Some(t_hash.clone())
};
view! { view! {
<tr <tr
class="hover group border-b border-base-200 cursor-context-menu" class=move || {
let base = "hover border-b border-base-200 transition-colors select-none";
if is_selected_fn() {
format!("{} bg-primary/10", base)
} else {
base.to_string()
}
}
on:contextmenu={ on:contextmenu={
let t_hash = t_hash.clone(); let t_hash = t_hash_click.clone();
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone()) move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
} }
on:click={
let t_hash = t_hash_click.clone();
move |_| set_selected_hash.set(Some(t_hash.clone()))
}
> >
<th>
<label>
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
</label>
</th>
<td class="font-medium truncate max-w-xs" title={t.name.clone()}> <td class="font-medium truncate max-w-xs" title={t.name.clone()}>
{t.name} {t.name}
</td> </td>
@@ -228,12 +235,87 @@ pub fn TorrentTable() -> impl IntoView {
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
</tbody> </tbody>
</table> </table>
</div>
<div class="md:hidden grid grid-cols-1 gap-3 p-3 pb-20 overflow-y-auto h-full">
{move || filtered_torrents().into_iter().map(|t| {
let progress_class = if t.percent_complete >= 100.0 { "progress-success" } else { "progress-primary" };
let status_str = format!("{:?}", t.status);
let status_badge_class = match t.status {
shared::TorrentStatus::Seeding => "badge-success badge-soft",
shared::TorrentStatus::Downloading => "badge-primary badge-soft",
shared::TorrentStatus::Paused => "badge-warning badge-soft",
shared::TorrentStatus::Error => "badge-error badge-soft",
_ => "badge-ghost"
};
let t_hash = t.hash.clone();
let t_hash_click = t.hash.clone();
let t_hash_ctx = t.hash.clone();
let is_selected_fn = move || {
selected_hash.get() == Some(t_hash.clone())
};
view! {
<div
class=move || {
let base = "card card-compact bg-base-100 shadow-sm border border-base-200 transition-all active:scale-[0.99]";
if is_selected_fn() {
format!("{} ring-2 ring-primary", base)
} else {
base.to_string()
}
}
on:contextmenu={
let t_hash = t_hash_ctx.clone();
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
}
on:click={
let t_hash = t_hash_click.clone();
move |_| set_selected_hash.set(Some(t_hash.clone()))
}
>
<div class="card-body gap-3">
<div class="flex justify-between items-start gap-2">
<h3 class="font-medium text-sm line-clamp-2 leading-tight">{t.name}</h3>
<div class={format!("badge badge-xs text-[10px] whitespace-nowrap {}", status_badge_class)}>
{status_str}
</div>
</div>
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[10px] opacity-70">
<span>{format_bytes(t.size)}</span>
<span>{format!("{:.1}%", t.percent_complete)}</span>
</div>
<progress class={format!("progress w-full h-1.5 {}", progress_class)} value={t.percent_complete} max="100"></progress>
</div>
<div class="grid grid-cols-3 gap-2 text-[10px] font-mono opacity-80 pt-1 border-t border-base-200/50">
<div class="flex flex-col">
<span class="text-[9px] opacity-60 uppercase">"Down"</span>
<span class="text-success">{format_speed(t.down_rate)}</span>
</div>
<div class="flex flex-col text-center border-l border-r border-base-200/50">
<span class="text-[9px] opacity-60 uppercase">"Up"</span>
<span class="text-primary">{format_speed(t.up_rate)}</span>
</div>
<div class="flex flex-col text-right">
<span class="text-[9px] opacity-60 uppercase">"ETA"</span>
<span>{if t.eta > 0 { format!("{}s", t.eta) } else { "".to_string() }}</span>
</div>
</div>
</div>
</div>
}
}).collect::<Vec<_>>()}
</div>
<Show when=move || menu_visible.get() fallback=|| ()> <Show when=move || menu_visible.get() fallback=|| ()>
<crate::components::context_menu::ContextMenu <crate::components::context_menu::ContextMenu
visible=true visible=true
position=menu_position.get() position=menu_position.get()
torrent_hash=active_hash.get() torrent_hash=selected_hash.get().unwrap_or_default() // Use selected_hash as source of truth
on_close=Callback::from(move |_| set_menu_visible.set(false)) on_close=Callback::from(move |_| set_menu_visible.set(false))
on_action=Callback::from(on_action) on_action=Callback::from(on_action)
/> />