feat(ui): modernize add download window

This commit is contained in:
NimBold
2026-06-19 18:39:28 +03:30
parent 6db21b7f14
commit d921876b72
2 changed files with 433 additions and 60 deletions
+86 -60
View File
@@ -687,15 +687,30 @@ export const AddDownloadsModal = () => {
icon: LucideIcon;
color: string;
}) => (
<div className="flex flex-col bg-bg-input/50 border border-border-modal/40 rounded-lg p-2.5 shadow-sm">
<div className="add-download-summary-card flex flex-col">
<div className="flex items-center gap-1.5 text-text-muted mb-1">
<Icon size={12} className={color} />
<span className="text-[10px] font-bold uppercase tracking-wider">{title}</span>
<span className="text-[10px] font-semibold uppercase tracking-wider">{title}</span>
</div>
<span className="text-sm font-semibold text-text-primary truncate">{value}</span>
</div>
);
const selectMediaFormat = (index: number) => {
if (selectedItemIndex === null) return;
const newItems = [...parsedItems];
const selectedItem = newItems[selectedItemIndex];
const format = selectedItem?.formats?.[index];
if (!selectedItem || !format) return;
selectedItem.selectedFormat = index;
selectedItem.size = format.detail || 'Unknown';
selectedItem.sizeBytes = format.bytes || 0;
const baseName = selectedItem.file.substring(0, selectedItem.file.lastIndexOf('.')) || selectedItem.file;
selectedItem.file = `${baseName}.${format.ext}`;
setParsedItems(newItems);
};
const requiredBytes = parsedItems.reduce((acc, item) => acc + (item.sizeBytes || 0), 0);
const requiredStr = requiredBytes > 0
? (requiredBytes < 1024 * 1024 ? `${(requiredBytes / 1024).toFixed(1)} KB`
@@ -716,24 +731,24 @@ export const AddDownloadsModal = () => {
/>
)}
<div className="app-modal-backdrop fixed inset-0 z-50 flex items-center justify-center">
<div className="app-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
<div className="app-modal add-download-modal w-[900px] h-[650px] flex flex-col overflow-hidden text-sm">
{/* Main Content Split */}
<div className="flex flex-1 overflow-hidden">
{/* Left Column: URLs and Preview */}
<div className="w-[55%] border-r border-border-modal flex flex-col bg-main-bg/50">
<div className="p-5 flex-1 flex flex-col gap-5">
<div className="add-download-left w-[55%] border-r border-border-modal flex flex-col">
<div className="add-download-pane p-5 flex-1 flex flex-col gap-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-text-primary font-semibold">
<div className="add-download-section-title flex items-center gap-2">
<Link size={16} className="text-blue-500" />
Download Links
</div>
</div>
<textarea
className="w-full h-32 bg-bg-input/80 border border-border-modal rounded-lg p-3 text-[13px] text-text-primary focus:outline-none focus:border-accent resize-none font-mono shadow-inner transition-colors"
className="add-download-control add-download-links-input w-full h-32 p-3 text-[13px] resize-none font-mono"
placeholder="Paste HTTP, HTTPS, FTP, or SFTP URLs here..."
value={urls}
onChange={(e) => setUrls(e.target.value)}
@@ -743,7 +758,7 @@ export const AddDownloadsModal = () => {
<button
type="button"
onClick={() => setMetadataRefreshNonce(value => value + 1)}
className="flex items-center gap-1.5 text-[11px] text-blue-500 hover:text-blue-400 font-medium"
className="add-download-link-button flex items-center gap-1.5 text-[11px] font-medium"
>
<RefreshCw size={12} /> Refresh Metadata
</button>
@@ -758,19 +773,19 @@ export const AddDownloadsModal = () => {
</div>
<div className="flex flex-col gap-2 flex-1 overflow-hidden">
<div className="flex items-center gap-2 text-text-primary font-semibold">
<div className="add-download-section-title flex items-center gap-2">
<ArrowRight size={16} className="text-blue-500" />
Preview
</div>
<div className="flex-1 border border-border-modal rounded-lg overflow-hidden bg-bg-input/30 flex flex-col">
<div className="bg-sidebar-bg/50 border-b border-border-modal px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
<div className="add-download-preview flex-1 overflow-hidden flex flex-col">
<div className="add-download-preview-header px-3 py-2 flex text-[11px] font-semibold text-text-muted uppercase tracking-wider">
<div className="flex-[2]">File</div>
<div className="flex-1">Size</div>
<div className="flex-[1.5]">Status</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
<div className="flex-1 overflow-y-auto p-2 space-y-1" role="listbox" aria-label="Download preview">
{parsedItems.length === 0 ? (
<div className="h-full flex items-center justify-center text-text-muted text-xs italic">
<div className="add-download-empty h-full flex items-center justify-center text-text-muted text-xs">
No links added yet.
</div>
) : (
@@ -778,10 +793,19 @@ export const AddDownloadsModal = () => {
<div
key={i}
onClick={() => setSelectedItemIndex(i)}
className={`flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md transition-all group ${
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setSelectedItemIndex(i);
}
}}
role="option"
aria-selected={selectedItemIndex === i}
tabIndex={0}
className={`add-download-preview-row flex flex-col text-xs px-2 py-2 cursor-pointer rounded-md group ${
selectedItemIndex === i
? 'bg-blue-500/10 border border-blue-500/30 shadow-sm'
: 'hover:bg-item-hover border border-transparent'
? 'is-selected border'
: 'border border-transparent'
}`}
>
<div className="flex items-center w-full">
@@ -808,16 +832,16 @@ export const AddDownloadsModal = () => {
</div>
{/* Right Column: Settings */}
<div className="w-[45%] flex flex-col overflow-y-auto bg-bg-modal">
<div className="p-6 space-y-7">
<div className="add-download-settings w-[45%] flex flex-col overflow-y-auto">
<div className="p-6 space-y-5">
{/* Media Format (Dynamic) */}
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
<section className="app-card relative overflow-hidden p-4">
<section className="add-download-section add-download-media-section relative overflow-hidden p-4">
<div className="absolute top-0 right-0 p-2 opacity-10">
<Video size={48} />
</div>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3 relative z-10">
<div className="add-download-section-title flex items-center gap-2 mb-3 relative z-10">
<Video size={16} className="text-purple-500" /> Media Format
</div>
@@ -830,25 +854,25 @@ export const AddDownloadsModal = () => {
<div className="space-y-3 relative z-10">
<div className="flex flex-col gap-1.5">
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">Available Streams</label>
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1">
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1" role="radiogroup" aria-label="Available media streams">
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
const Icon = f.type === 'Audio' ? Music : Film;
return (
<div
key={idx}
onClick={() => {
const newItems = [...parsedItems];
newItems[selectedItemIndex].selectedFormat = idx;
newItems[selectedItemIndex].size = f.detail || 'Unknown';
newItems[selectedItemIndex].sizeBytes = f.bytes || 0;
// Update filename extension
const baseName = newItems[selectedItemIndex].file.substring(0, newItems[selectedItemIndex].file.lastIndexOf('.')) || newItems[selectedItemIndex].file;
newItems[selectedItemIndex].file = `${baseName}.${f.ext}`;
setParsedItems(newItems);
onClick={() => selectMediaFormat(idx)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
selectMediaFormat(idx);
}
}}
className={`flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer text-xs border transition-all ${
isSelected ? 'bg-purple-500/10 border-purple-500/30 text-purple-600 dark:text-purple-400 font-semibold shadow-sm' : 'bg-bg-input border-border-modal text-text-secondary hover:border-border-modal/80 hover:bg-item-hover/50'
role="radio"
aria-checked={isSelected}
tabIndex={0}
className={`add-download-format-row flex items-center justify-between px-3 py-2 cursor-pointer text-xs border ${
isSelected ? 'is-selected text-purple-600 dark:text-purple-400 font-semibold' : 'text-text-secondary'
}`}
>
<div className="flex items-center gap-2 min-w-0">
@@ -874,8 +898,8 @@ export const AddDownloadsModal = () => {
)}
{/* Save Location */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<FolderPlus size={16} className="text-blue-500" /> Save Location
</div>
<div className="flex gap-2">
@@ -883,11 +907,12 @@ export const AddDownloadsModal = () => {
type="text"
readOnly
value={saveLocation}
className="flex-1 bg-bg-input border border-border-modal rounded-md px-3 py-1.5 text-xs text-text-muted font-mono"
className="add-download-control flex-1 px-3 py-1.5 text-xs text-text-muted font-mono"
aria-label="Save location"
/>
<button
onClick={handleBrowse}
className="bg-item-hover hover:bg-item-hover/80 text-text-primary border border-border-modal px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
className="add-download-button add-download-button-secondary px-3 text-xs font-medium"
>
Browse
</button>
@@ -895,14 +920,14 @@ export const AddDownloadsModal = () => {
</section>
{/* Transfer Settings */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<Settings size={16} className="text-blue-500" /> Transfer Settings
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Target Queue</label>
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="w-32 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs text-text-primary focus:border-accent focus:outline-none">
<select value={selectedQueueId} onChange={e=>setSelectedQueueId(e.target.value)} className="add-download-control add-download-select w-32 px-2 py-1 text-xs" aria-label="Target queue">
{queues.map(q => (
<option key={q.id} value={q.id}>{q.name}</option>
))}
@@ -912,19 +937,19 @@ export const AddDownloadsModal = () => {
<div className="flex items-center justify-between">
<label className="text-xs text-text-secondary font-medium">Connections per File</label>
<div className="flex items-center gap-2">
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="w-24 accent-blue-500" disabled={parsedItems.some(i => i.isMedia)} />
<span className="text-xs text-text-primary font-mono w-4 text-right">{connections}</span>
<input type="range" min="1" max="16" value={connections} onChange={e=>setConnections(Number(e.target.value))} className="add-download-range w-24" disabled={parsedItems.some(i => i.isMedia)} aria-label="Connections per file" />
<span className="add-download-value text-xs text-text-primary font-mono w-6 text-center">{connections}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={speedLimitEnabled} onChange={e=>setSpeedLimitEnabled(e.target.checked)} className="add-download-checkbox" />
Limit speed per file
</label>
{speedLimitEnabled && (
<div className="flex items-center gap-1.5">
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2 py-1 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="number" value={speedLimit} onChange={e=>setSpeedLimit(e.target.value)} className="add-download-control w-16 px-2 py-1 text-xs font-mono" aria-label="Speed limit per file" />
<span className="text-[10px] text-text-muted">KiB/s</span>
</div>
)}
@@ -933,28 +958,29 @@ export const AddDownloadsModal = () => {
</section>
{/* Authorization */}
<section>
<div className="flex items-center gap-2 text-sm font-semibold text-text-primary mb-3">
<section className="add-download-section">
<div className="add-download-section-title flex items-center gap-2 mb-3">
<Shield size={16} className="text-blue-500" /> Authorization
</div>
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer mb-3">
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={useAuth} onChange={e=>setUseAuth(e.target.checked)} className="add-download-checkbox" />
Use authorization
</label>
{useAuth && (
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder="Username" className="add-download-control w-full px-3 py-1.5 text-xs" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Password" className="add-download-control w-full px-3 py-1.5 text-xs" />
</div>
)}
</section>
{/* Advanced */}
<section className="pt-2 border-t border-border-modal/50">
<section className="add-download-section add-download-advanced">
<button
onClick={() => setAdvancedExpanded(!advancedExpanded)}
className="flex items-center gap-2 text-sm font-semibold text-text-primary w-full hover:text-blue-500 transition-colors"
className="add-download-advanced-toggle flex items-center gap-2 text-sm font-semibold text-text-primary w-full"
aria-expanded={advancedExpanded}
>
{advancedExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
Advanced Transfer
@@ -963,30 +989,30 @@ export const AddDownloadsModal = () => {
{advancedExpanded && (
<div className="mt-4 space-y-4 pl-6">
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="rounded border-border-modal text-blue-500 focus:ring-blue-500/20" />
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="add-download-checkbox" />
Verify Checksum
</label>
{checksumEnabled && (
<div className="flex gap-2">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="w-24 bg-bg-input border border-border-modal rounded-lg px-2 text-xs text-text-primary focus:border-accent focus:outline-none">
<select value={checksumAlgo} onChange={e=>setChecksumAlgo(e.target.value)} className="add-download-control add-download-select w-24 px-2 text-xs" aria-label="Checksum algorithm">
<option>MD5</option><option>SHA-1</option><option>SHA-256</option>
</select>
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="flex-1 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={checksumValue} onChange={e=>setChecksumValue(e.target.value)} placeholder="Expected digest" className="add-download-control flex-1 px-3 py-1.5 text-xs font-mono" />
</div>
)}
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Headers</label>
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
<textarea value={headers} onChange={e=>setHeaders(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Request headers" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Cookies</label>
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="w-full bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none" />
<input type="text" value={cookies} onChange={e=>setCookies(e.target.value)} placeholder="name=value; other=value" className="add-download-control w-full px-3 py-1.5 text-xs font-mono" aria-label="Cookies" />
</div>
<div>
<label className="block text-[10px] uppercase font-bold tracking-wider text-text-muted mb-1">Mirrors</label>
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="w-full h-12 bg-bg-input border border-border-modal rounded-lg px-3 py-1.5 text-xs font-mono text-text-primary focus:border-accent focus:outline-none resize-none" />
<textarea value={mirrors} onChange={e=>setMirrors(e.target.value)} className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none" aria-label="Mirrors" />
</div>
</div>
)}
@@ -997,25 +1023,25 @@ export const AddDownloadsModal = () => {
</div>
{/* Footer */}
<div className="p-4 bg-sidebar-bg/50 border-t border-border-modal flex items-center shrink-0">
<div className="add-download-footer p-4 flex items-center shrink-0">
<div className="text-[11px] text-text-muted font-medium flex-1">
{parsedItems.length === 0 ? "Paste one or more links." : `Ready to add ${parsedItems.length} download(s).`}
</div>
<div className="flex gap-2.5">
<button onClick={() => toggleAddModal(false)} className="app-button border-transparent bg-transparent px-4 text-xs text-text-secondary">
<button onClick={() => toggleAddModal(false)} className="add-download-button add-download-button-cancel px-4 text-xs">
Cancel
</button>
<button
onClick={() => handleStart(false)}
disabled={parsedItems.length === 0}
className="app-button px-4 text-xs disabled:opacity-50"
className="add-download-button add-download-button-secondary px-4 text-xs"
>
Add to Queue
</button>
<button
onClick={() => handleStart(true)}
disabled={parsedItems.length === 0}
className="app-button app-button-primary px-5 text-xs disabled:opacity-50"
className="add-download-button add-download-button-primary px-5 text-xs"
>
<Play size={12} fill="currentColor" /> Start Downloads
</button>
+347
View File
@@ -376,6 +376,353 @@
animation: modal-in 150ms ease-out;
}
/* Add Download window */
.add-download-modal {
--add-surface: hsl(var(--surface-raised) / 0.72);
--add-highlight: hsl(0 0% 100% / 0.08);
background:
linear-gradient(180deg, hsl(var(--surface-overlay) / 0.94), hsl(var(--bg-modal) / 0.98));
backdrop-filter: blur(22px) saturate(1.12);
-webkit-backdrop-filter: blur(22px) saturate(1.12);
border-color: hsl(var(--border-modal));
border-radius: 14px;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.08),
0 28px 70px hsl(var(--shadow-color)),
0 2px 10px hsl(var(--shadow-color));
}
.add-download-left {
background:
radial-gradient(circle at 18% 0%, hsl(var(--accent-color) / 0.055), transparent 34%),
hsl(var(--main-bg) / 0.56);
}
.add-download-settings {
background: hsl(var(--bg-modal) / 0.7);
}
.add-download-section-title {
color: hsl(var(--text-primary));
font-size: 13px;
font-weight: 650;
letter-spacing: -0.005em;
}
.add-download-section {
border: 1px solid hsl(var(--border-modal));
border-radius: 11px;
background:
linear-gradient(180deg, var(--add-highlight), transparent 38%),
var(--add-surface);
padding: 14px;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
0 1px 2px hsl(var(--shadow-color));
}
.add-download-control {
min-height: 30px;
border: 1px solid hsl(var(--border-modal));
border-radius: 7px;
background:
linear-gradient(180deg, hsl(0 0% 100% / 0.035), transparent),
hsl(var(--bg-input) / 0.9);
color: hsl(var(--text-primary));
box-shadow:
inset 0 1px 2px hsl(var(--shadow-color)),
0 1px 0 hsl(0 0% 100% / 0.035);
transition:
border-color 120ms ease,
background-color 120ms ease,
box-shadow 120ms ease;
}
.add-download-control:hover:not(:disabled):not(:read-only) {
border-color: hsl(var(--text-muted) / 0.7);
}
.add-download-control:focus,
.add-download-control:focus-visible {
border-color: hsl(var(--accent-color) / 0.9);
outline: none;
box-shadow:
0 0 0 3px hsl(var(--accent-color) / 0.18),
inset 0 1px 2px hsl(var(--shadow-color));
}
.add-download-control:read-only {
background: hsl(var(--bg-input) / 0.62);
}
.add-download-control::placeholder {
color: hsl(var(--text-muted) / 0.82);
}
.add-download-links-input {
border-radius: 10px;
line-height: 1.55;
}
.add-download-link-button {
border-radius: 6px;
color: hsl(var(--accent-color));
padding: 3px 5px;
transition: background-color 120ms ease, color 120ms ease;
}
.add-download-link-button:hover {
background: hsl(var(--accent-color) / 0.1);
}
.add-download-summary-card {
min-width: 0;
padding: 10px 11px;
border: 1px solid hsl(var(--border-modal));
border-radius: 10px;
background:
linear-gradient(145deg, hsl(0 0% 100% / 0.055), transparent 55%),
var(--add-surface);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
0 1px 2px hsl(var(--shadow-color));
}
.add-download-preview {
border: 1px solid hsl(var(--border-modal));
border-radius: 11px;
background: hsl(var(--bg-input) / 0.35);
box-shadow: inset 0 1px 2px hsl(var(--shadow-color));
}
.add-download-preview-header {
border-bottom: 1px solid hsl(var(--border-modal));
background: hsl(var(--surface-raised) / 0.66);
}
.add-download-preview-row {
transition:
background-color 100ms ease,
border-color 100ms ease,
box-shadow 100ms ease;
}
.add-download-preview-row:hover {
background: hsl(var(--item-hover));
}
.add-download-preview-row.is-selected {
border-color: hsl(var(--accent-color) / 0.34);
background: hsl(var(--accent-color) / 0.11);
box-shadow: inset 3px 0 0 hsl(var(--accent-color) / 0.85);
}
.add-download-preview-row:focus-visible,
.add-download-format-row:focus-visible {
outline: 2px solid hsl(var(--accent-color) / 0.6);
outline-offset: -1px;
}
.add-download-empty {
color: hsl(var(--text-muted) / 0.86);
}
.add-download-media-section {
border-color: hsl(272 70% 62% / 0.28);
background:
radial-gradient(circle at 100% 0%, hsl(272 75% 60% / 0.09), transparent 46%),
var(--add-surface);
}
.add-download-format-row {
border-color: hsl(var(--border-modal));
border-radius: 8px;
background: hsl(var(--bg-input) / 0.7);
transition:
background-color 100ms ease,
border-color 100ms ease,
box-shadow 100ms ease;
}
.add-download-format-row:hover {
background: hsl(var(--item-hover));
border-color: hsl(var(--text-muted) / 0.55);
}
.add-download-format-row.is-selected {
border-color: hsl(272 74% 62% / 0.42);
background: hsl(272 74% 58% / 0.12);
box-shadow: inset 3px 0 0 hsl(272 74% 62% / 0.9);
}
.add-download-select {
appearance: auto;
cursor: default;
}
.add-download-value {
min-width: 25px;
padding: 2px 4px;
border: 1px solid hsl(var(--border-modal));
border-radius: 5px;
background: hsl(var(--bg-input) / 0.68);
}
.add-download-checkbox {
appearance: none;
width: 15px;
height: 15px;
flex: 0 0 15px;
border: 1px solid hsl(var(--border-modal));
border-radius: 4px;
background: hsl(var(--bg-input) / 0.92);
box-shadow: inset 0 1px 1px hsl(var(--shadow-color));
cursor: default;
transition:
border-color 100ms ease,
background-color 100ms ease,
box-shadow 100ms ease;
}
.add-download-checkbox:hover {
border-color: hsl(var(--text-muted) / 0.8);
}
.add-download-checkbox:checked {
border-color: hsl(var(--accent-color));
background-color: hsl(var(--accent-color));
background-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='m2.4 6.2 2.2 2.2 5-5' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
}
.add-download-checkbox:focus-visible {
outline: none;
box-shadow:
0 0 0 3px hsl(var(--accent-color) / 0.2),
inset 0 1px 1px hsl(var(--shadow-color));
}
.add-download-range {
height: 16px;
accent-color: hsl(var(--accent-color));
}
.add-download-range:disabled {
opacity: 0.42;
}
.add-download-advanced {
padding: 0;
overflow: hidden;
}
.add-download-advanced-toggle {
min-height: 42px;
padding: 0 14px;
border-radius: 10px;
transition: background-color 100ms ease, color 100ms ease;
}
.add-download-advanced-toggle:hover {
background: hsl(var(--item-hover));
color: hsl(var(--accent-color));
}
.add-download-advanced > div {
padding: 0 14px 14px;
}
.add-download-footer {
border-top: 1px solid hsl(var(--border-modal));
background:
linear-gradient(180deg, hsl(0 0% 100% / 0.035), transparent),
hsl(var(--surface-raised) / 0.78);
box-shadow: 0 -1px 0 hsl(0 0% 100% / 0.025);
}
.add-download-button {
display: inline-flex;
min-height: 30px;
align-items: center;
justify-content: center;
gap: 6px;
border: 1px solid transparent;
border-radius: 7px;
color: hsl(var(--text-primary));
font-weight: 560;
letter-spacing: -0.005em;
transition:
background-color 100ms ease,
border-color 100ms ease,
box-shadow 100ms ease,
transform 70ms ease,
opacity 100ms ease;
}
.add-download-button:active:not(:disabled) {
transform: translateY(1px);
}
.add-download-button-secondary {
border-color: hsl(var(--border-modal));
background:
linear-gradient(180deg, hsl(0 0% 100% / 0.055), transparent),
hsl(var(--bg-input) / 0.88);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.055),
0 1px 2px hsl(var(--shadow-color));
}
.add-download-button-secondary:hover:not(:disabled) {
border-color: hsl(var(--text-muted) / 0.65);
background:
linear-gradient(hsl(var(--item-hover)), hsl(var(--item-hover))),
hsl(var(--bg-input));
}
.add-download-button-cancel {
color: hsl(var(--text-secondary));
}
.add-download-button-cancel:hover:not(:disabled) {
background: hsl(var(--item-hover));
color: hsl(var(--text-primary));
}
.add-download-button-primary {
border-color: hsl(var(--accent-color) / 0.86);
background:
linear-gradient(180deg, hsl(0 0% 100% / 0.16), transparent 52%),
hsl(var(--accent-color));
color: white;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.18),
0 1px 2px hsl(var(--accent-color) / 0.32);
}
.add-download-button-primary:hover:not(:disabled) {
background:
linear-gradient(180deg, hsl(0 0% 100% / 0.2), transparent 52%),
hsl(var(--accent-color) / 0.92);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.2),
0 2px 5px hsl(var(--accent-color) / 0.3);
}
.add-download-button:disabled {
opacity: 0.42;
filter: saturate(0.55);
}
.add-download-button:focus-visible,
.add-download-link-button:focus-visible,
.add-download-advanced-toggle:focus-visible {
outline: 2px solid hsl(var(--accent-color) / 0.65);
outline-offset: 2px;
}
.app-toast {
border: 1px solid hsl(var(--border-modal));
border-radius: 8px;