style(ui): polish desktop interface design and interactions

- Replace abrupt sidebar toggle with modern negative-margin swipe animation
- Match stripe styling to original macOS native application with pill-shaped rows and proper faint opacity
- Remove borders between rows and background stripes
- Add dynamic flex-1 CSS clipping trick to perfectly fill viewport with stripes without causing infinite scroll
- Enhance empty state text with physical-looking shortcut keycaps, Lucide icons, OS detection, and accent colors
- Fix background color seam behind sidebar panel in Dark theme
- Restore bright accent colors for active navigation items
This commit is contained in:
NimBold
2026-06-14 13:11:41 +03:30
parent b1b17b7ad8
commit 77fdc30b80
4 changed files with 79 additions and 38 deletions
+6 -3
View File
@@ -265,10 +265,13 @@ function App() {
return (
<div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary">
<div
className={`app-sidebar-shell relative z-20 shrink-0 ${
isSidebarVisible ? 'opacity-100' : 'w-0 opacity-0 pointer-events-none'
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
style={isSidebarVisible ? { width: sidebarWidth } : undefined}
style={{
width: sidebarWidth,
marginLeft: isSidebarVisible ? 0 : -sidebarWidth
}}
>
<div className="app-sidebar-panel h-full w-full">
<Sidebar
+27 -7
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useDownloadStore, DownloadItem } from '../store/useDownloadStore';
import { useSettingsStore } from '../store/useSettingsStore';
import { SidebarFilter } from './Sidebar';
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle } from 'lucide-react';
import { Play, Pause, Plus, Trash2, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle, Command } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { homeDir } from '@tauri-apps/api/path';
@@ -14,6 +14,8 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const isMac = navigator.userAgent.includes('Mac');
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [columnWidths, setColumnWidths] = useState([340, 100, 220, 100, 80, 170]);
const columnMinimums = [200, 80, 170, 80, 70, 120];
@@ -190,8 +192,18 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="downloads-empty-state">
<ArrowDownCircle aria-hidden="true" />
<div className="downloads-empty-title">No Downloads</div>
<div className="downloads-empty-description">
Use Add or press <span className="keyboard-symbol"></span>V to paste one or more links.
<div className="downloads-empty-description flex items-center justify-center mt-2.5 text-[13px] text-text-muted">
Click <Plus size={15} className="text-accent stroke-[3] mx-1.5" /> button or
<span className="flex items-center mx-1.5">
<span className="flex items-center justify-center px-1.5 py-0.5 bg-item-hover rounded border border-border-color shadow-sm min-w-[22px] min-h-[22px]">
{isMac ? <Command size={12} strokeWidth={2.5} className="text-text-primary" /> : <span className="text-[10px] font-bold text-text-primary">Ctrl</span>}
</span>
<span className="text-accent font-bold mx-1.5 text-[14px]">+</span>
<span className="flex items-center justify-center px-1.5 py-0.5 bg-item-hover rounded border border-border-color shadow-sm min-w-[22px] min-h-[22px]">
<span className="text-[11px] font-bold text-text-primary">V</span>
</span>
</span>
to add downloads
</div>
</div>
) : (
@@ -210,7 +222,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
<div className="download-table-body">
<div className="h-full overflow-auto">
<div className="h-full overflow-auto flex flex-col">
{filteredDownloads.map(d => (
<div
key={d.id}
@@ -294,9 +306,17 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
</div>
</div>
))}
{Array.from({ length: Math.max(0, 10 - filteredDownloads.length) }).map((_, index) => (
<div key={`ghost-${index}`} className="download-ghost-row" />
))}
<div className="flex-1 overflow-hidden flex flex-col pointer-events-none">
{Array.from({ length: 50 }).map((_, index) => {
const isEven = (filteredDownloads.length + index) % 2 === 1;
return (
<div
key={`ghost-${index}`}
className={`download-ghost-row ${isEven ? 'striped' : ''}`}
/>
);
})}
</div>
</div>
</div>
</div>
+29 -19
View File
@@ -247,7 +247,7 @@ export default function SettingsView() {
type="number" min="1" max="16"
value={settings.perServerConnections}
onChange={(e) => settings.setPerServerConnections(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
<div className="mac-settings-row">
@@ -259,7 +259,7 @@ export default function SettingsView() {
type="number" min="1" max="12"
value={settings.maxConcurrentDownloads}
onChange={(e) => settings.setMaxConcurrentDownloads(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
<div className="mac-settings-row">
@@ -267,15 +267,15 @@ export default function SettingsView() {
<span>Global speed limit:</span>
<small>0 = unlimited speed</small>
</div>
<div className="flex items-center gap-2">
<input
type="text"
value={settings.globalSpeedLimit}
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
placeholder="0"
className="app-control w-20 text-right font-mono px-2"
/>
<span className="text-[12px] text-text-muted">KiB/s</span>
<div className="relative">
<input
type="text"
value={settings.globalSpeedLimit}
onChange={(e) => settings.setGlobalSpeedLimit(e.target.value)}
placeholder="0"
className="app-control w-24 text-center font-mono pr-9"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-text-muted pointer-events-none">KiB/s</span>
</div>
</div>
<div className="mac-settings-row">
@@ -287,7 +287,7 @@ export default function SettingsView() {
type="number" min="0" max="10"
value={settings.maxAutomaticRetries}
onChange={(e) => settings.setMaxAutomaticRetries(Number(e.target.value))}
className="app-control w-16 text-center"
className="app-control w-24 text-center"
/>
</div>
</div>
@@ -476,13 +476,23 @@ export default function SettingsView() {
<div className="mac-settings-group">
<div className="mac-settings-row">
<span className="text-[13px] text-text-primary">Custom User Agent</span>
<input
type="text"
value={settings.customUserAgent}
onChange={(e) => settings.setCustomUserAgent(e.target.value)}
placeholder="e.g. Mozilla/5.0..."
className="app-control flex-1 ml-4 font-mono text-[11px]"
/>
<div className="flex-1 ml-4 relative">
<input
type="text"
list="user-agents"
value={settings.customUserAgent}
onChange={(e) => settings.setCustomUserAgent(e.target.value)}
placeholder="e.g. Mozilla/5.0..."
className="app-control w-full font-mono text-[11px]"
/>
<datalist id="user-agents">
<option value="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36">Chrome (Windows)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36">Chrome (macOS)</option>
<option value="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0">Firefox (Windows)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0">Firefox (macOS)</option>
<option value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15">Safari (macOS)</option>
</datalist>
</div>
</div>
<p className="settings-group-footer">Spoofs the browser User-Agent to bypass download restrictions. Leave blank for default.</p>
</div>
+17 -9
View File
@@ -9,6 +9,7 @@
--item-hover: 0 0% 0% / 0.05;
--item-selected: 211 100% 50%;
--accent-color: 211 100% 50%;
--stripe-bg: 0 0% 0% / 0.04;
--text-primary: 0 0% 10%;
--text-secondary: 0 0% 40%;
@@ -36,6 +37,7 @@
--item-hover: 0 0% 0% / 0.05;
--item-selected: 211 100% 50%;
--accent-color: 211 100% 50%;
--stripe-bg: 0 0% 0% / 0.04;
--text-primary: 0 0% 10%;
--text-secondary: 0 0% 40%;
--text-muted: 0 0% 55%;
@@ -55,7 +57,7 @@
.theme-dark {
color-scheme: dark;
--main-bg: 0 0% 10%;
--main-bg: 0 0% 11%;
--sidebar-bg: 0 0% 13%;
--sidebar-glass: 0 0% 13%;
--surface-raised: 0 0% 12%;
@@ -65,6 +67,7 @@
--item-hover: 0 0% 100% / 0.07;
--item-selected: 211 100% 56%;
--accent-color: 211 100% 56%;
--stripe-bg: 0 0% 100% / 0.04;
--text-primary: 0 0% 91%;
--text-secondary: 0 0% 75%;
--text-muted: 0 0% 50%;
@@ -95,6 +98,7 @@
--item-hover: 326 100% 74% / 0.10;
--item-selected: 326 100% 74%;
--accent-color: 326 100% 74%;
--stripe-bg: 0 0% 100% / 0.04;
--text-primary: 60 30% 96%;
--text-secondary: 228 14% 74%;
--text-muted: 229 12% 58%;
@@ -125,6 +129,7 @@
--item-hover: 193 43% 67% / 0.12;
--item-selected: 193 43% 67%;
--accent-color: 193 43% 67%;
--stripe-bg: 0 0% 100% / 0.04;
--text-primary: 218 27% 92%;
--text-secondary: 219 28% 80%;
--text-muted: 218 17% 63%;
@@ -376,7 +381,7 @@
.app-sidebar-shell {
padding: 9px 0 9px 9px;
background: hsl(var(--sidebar-shell-bg));
background: transparent;
}
.sidebar-resize-handle {
@@ -960,17 +965,18 @@
}
.download-row {
height: 31px;
height: 32px;
display: grid;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid hsl(var(--border-color));
margin: 2px 12px;
border-radius: 6px;
color: hsl(var(--text-primary));
font-size: 12px;
}
.download-row:nth-child(even) {
background: hsl(var(--item-hover));
background: hsl(var(--stripe-bg));
}
.download-row:hover {
@@ -1014,12 +1020,14 @@
}
.download-ghost-row {
height: 31px;
border-bottom: 1px solid hsl(var(--border-color));
height: 32px;
margin: 2px 12px;
border-radius: 6px;
flex-shrink: 0;
}
.download-ghost-row:nth-child(even) {
background: hsl(var(--item-hover));
.download-ghost-row.striped {
background: hsl(var(--stripe-bg));
}
.download-status-cell {