refactor(webui): lean import buttons on shared styles

- move the import page over to shared button variants and option buttons
- strip route-local button chrome back to layout-only helpers
- keep the import route styling focused on layout, cards, and state indicators
This commit is contained in:
Antti Kettunen 2026-05-16 21:38:11 +03:00
parent b78350a3e2
commit d066aba03d
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
7 changed files with 70 additions and 273 deletions

View file

@ -408,6 +408,14 @@
box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08); box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08);
} }
.optionButtonSelected:hover {
border-color: rgba(var(--accent-light-rgb), 0.62);
background: rgba(var(--accent-rgb), 0.26);
box-shadow:
0 0 0 1px rgba(var(--accent-light-rgb), 0.12),
0 10px 24px rgba(0, 0, 0, 0.14);
}
.button { .button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -498,6 +506,29 @@
color: rgba(0, 0, 0, 0.55); color: rgba(0, 0, 0, 0.55);
} }
.buttonSecondary {
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.buttonSecondary:hover:not(:disabled) {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.buttonSecondary:focus-visible {
border-color: rgba(var(--accent-light-rgb), 0.45);
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
}
.buttonSecondary:disabled {
opacity: 0.55;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.45);
}
.buttonGhost { .buttonGhost {
border-color: transparent; border-color: transparent;
background: transparent; background: transparent;

View file

@ -266,7 +266,7 @@ type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;
export type ButtonProps = Omit<BaseButtonProps, 'className'> & { export type ButtonProps = Omit<BaseButtonProps, 'className'> & {
className?: string; className?: string;
size?: 'sm' | 'md' | 'lg' | 'icon'; size?: 'sm' | 'md' | 'lg' | 'icon';
variant?: 'default' | 'primary' | 'ghost'; variant?: 'default' | 'primary' | 'secondary' | 'ghost';
}; };
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button( export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
@ -284,6 +284,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
[styles.buttonSizeLg]: size === 'lg', [styles.buttonSizeLg]: size === 'lg',
[styles.buttonIcon]: size === 'icon', [styles.buttonIcon]: size === 'icon',
[styles.buttonPrimary]: variant === 'primary', [styles.buttonPrimary]: variant === 'primary',
[styles.buttonSecondary]: variant === 'secondary',
[styles.buttonGhost]: variant === 'ghost', [styles.buttonGhost]: variant === 'ghost',
}, },
className, className,

View file

@ -301,6 +301,7 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon"
className={albumResults === null ? styles.hidden : ''} className={albumResults === null ? styles.hidden : ''}
id="import-page-album-clear-btn" id="import-page-album-clear-btn"
title="Clear search" title="Clear search"
@ -436,14 +437,16 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
<div className={styles.importPageMatchActions}> <div className={styles.importPageMatchActions}>
<Button <Button
type="button" type="button"
className={styles.importPageSecondaryBtn} variant="secondary"
size="sm"
onClick={onAutoRematch} onClick={onAutoRematch}
> >
Re-match Automatically Re-match Automatically
</Button> </Button>
<Button <Button
type="button" type="button"
className={styles.importPageBackBtn} variant="ghost"
size="sm"
onClick={onBackToSearch} onClick={onBackToSearch}
> >
Back to Search Back to Search
@ -509,7 +512,8 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
{file ? ( {file ? (
<Button <Button
type="button" type="button"
className={styles.importPageMatchUnmatch} variant="ghost"
size="icon"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onUnmatchTrack(index); onUnmatchTrack(index);

View file

@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Button, RangeInput, Select, Switch } from '@/components/form/form'; import { Button, OptionButton, OptionButtonGroup, RangeInput, Select, Switch } from '@/components/form/form';
import type { import type {
ImportAutoFilter, ImportAutoFilter,
@ -171,10 +171,12 @@ export function AutoImportPanel({
> >
{getAutoImportStatusText(statusQuery.data)} {getAutoImportStatusText(statusQuery.data)}
</span> </span>
<div className={styles.importPageFlexSpacer} />
{statusQuery.data?.running ? ( {statusQuery.data?.running ? (
<Button <Button
type="button" type="button"
className={styles.autoImportScanNowBtn} variant="secondary"
size="sm"
id="auto-import-scan-now" id="auto-import-scan-now"
title="Scan import folder now" title="Scan import folder now"
disabled={scanMutation.isPending} disabled={scanMutation.isPending}
@ -215,7 +217,8 @@ export function AutoImportPanel({
</div> </div>
<Button <Button
type="button" type="button"
className={`${styles.autoImportActionBtn} ${styles.autoImportActionSecondary}`} variant="primary"
size="sm"
disabled={saveSettingsMutation.isPending} disabled={saveSettingsMutation.isPending}
onClick={() => onClick={() =>
saveSettingsMutation.mutate({ saveSettingsMutation.mutate({
@ -264,25 +267,22 @@ export function AutoImportPanel({
{counts.failed} failed {counts.failed} failed
</span> </span>
</div> </div>
<div className={styles.autoImportFilters} id="auto-import-filters"> <OptionButtonGroup className={styles.autoImportFilters}>
{(['all', 'pending', 'imported', 'failed'] as const).map((filter) => ( {(['all', 'pending', 'imported', 'failed'] as const).map((filter) => (
<Button <OptionButton
key={filter} key={filter}
type="button" selected={autoFilter === filter}
className={`${styles.autoImportFilterPill} ${
autoFilter === filter ? styles.active : ''
}`}
data-filter={filter}
onClick={() => onFilterChange(filter)} onClick={() => onFilterChange(filter)}
> >
{filter === 'pending' ? 'Needs Review' : titleCase(filter)} {filter === 'pending' ? 'Needs Review' : titleCase(filter)}
</Button> </OptionButton>
))} ))}
<div className={styles.importPageFlexSpacer} /> <div className={styles.importPageFlexSpacer} />
{counts.review > 0 ? ( {counts.review > 0 ? (
<Button <Button
type="button" type="button"
className={styles.autoImportBatchBtn} variant="secondary"
size="sm"
id="auto-import-approve-all" id="auto-import-approve-all"
disabled={approveAllMutation.isPending} disabled={approveAllMutation.isPending}
onClick={() => approveAllMutation.mutate()} onClick={() => approveAllMutation.mutate()}
@ -293,7 +293,8 @@ export function AutoImportPanel({
{counts.imported + counts.failed > 0 ? ( {counts.imported + counts.failed > 0 ? (
<Button <Button
type="button" type="button"
className={`${styles.autoImportBatchBtn} ${styles.autoImportClearBtn}`} variant="ghost"
size="sm"
id="auto-import-clear-completed" id="auto-import-clear-completed"
disabled={clearMutation.isPending} disabled={clearMutation.isPending}
onClick={() => clearMutation.mutate()} onClick={() => clearMutation.mutate()}
@ -301,7 +302,7 @@ export function AutoImportPanel({
Clear History Clear History
</Button> </Button>
) : null} ) : null}
</div> </OptionButtonGroup>
</> </>
) : null} ) : null}
@ -449,7 +450,8 @@ function AutoImportResultCard({
<div className={styles.autoImportActions}> <div className={styles.autoImportActions}>
<Button <Button
type="button" type="button"
className={`${styles.autoImportActionBtn} ${styles.autoImportActionPrimary}`} variant="primary"
size="sm"
disabled={approvePending} disabled={approvePending}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
@ -460,7 +462,8 @@ function AutoImportResultCard({
</Button> </Button>
<Button <Button
type="button" type="button"
className={`${styles.autoImportActionBtn} ${styles.autoImportActionSecondary}`} variant="secondary"
size="sm"
disabled={rejectPending} disabled={rejectPending}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();

View file

@ -44,30 +44,6 @@
animation: page-title-shimmer 6s ease-in-out infinite; animation: page-title-shimmer 6s ease-in-out infinite;
} }
.importPageRefreshBtn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
color: #ccc;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.importPageRefreshBtn:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.importPageRefreshBtnRefreshing {
opacity: 0.72;
cursor: progress;
}
.importPageStagingBar { .importPageStagingBar {
display: flex; display: flex;
align-items: center; align-items: center;
@ -175,12 +151,6 @@
border-color: rgba(var(--accent-light-rgb), 0.5); border-color: rgba(var(--accent-light-rgb), 0.5);
} }
.importPageSearchBar > button {
align-self: stretch;
min-width: 40px;
padding: 0 12px;
}
/* Album grid */ /* Album grid */
.importPageAlbumGrid { .importPageAlbumGrid {
display: grid; display: grid;
@ -416,25 +386,6 @@
font-style: italic; font-style: italic;
} }
.importPageMatchUnmatch {
background: none;
border: none;
color: rgba(255, 255, 255, 0.3);
font-size: 14px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.importPageMatchUnmatch:hover {
color: #ff4444;
background: rgba(255, 68, 68, 0.1);
}
/* Unmatched file pool */ /* Unmatched file pool */
.importPageUnmatchedPool { .importPageUnmatchedPool {
padding: 16px; padding: 16px;
@ -508,30 +459,6 @@
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
} }
.importPageSecondaryBtn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #ccc;
transition: all 0.2s;
}
.importPageSecondaryBtn:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.importPageBackBtn {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.5);
transition: all 0.2s;
}
.importPageBackBtn:hover {
color: #fff;
border-color: rgba(255, 255, 255, 0.2);
}
/* Singles */ /* Singles */
.importPageSinglesHeader { .importPageSinglesHeader {
display: flex; display: flex;
@ -631,25 +558,6 @@
flex-shrink: 0; flex-shrink: 0;
} }
.importPageIdentifyBtn {
padding: 6px 12px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: #ccc;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.importPageIdentifyBtn:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
/* Inline search panel for singles */ /* Inline search panel for singles */
.importPageSingleSearchPanel { .importPageSingleSearchPanel {
grid-column: 1 / -1; grid-column: 1 / -1;
@ -677,17 +585,6 @@
border-color: rgba(var(--accent-light-rgb), 0.4); border-color: rgba(var(--accent-light-rgb), 0.4);
} }
.importPageSingleSearchGo {
padding: 8px 14px;
background: rgb(var(--accent-light-rgb));
border: none;
border-radius: 8px;
color: #000;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.importPageSingleSearchResults { .importPageSingleSearchResults {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -796,19 +693,6 @@
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.importPageQueueClear {
background: none;
border: none;
color: rgba(255, 255, 255, 0.3);
font-size: 12px;
cursor: pointer;
padding: 2px 8px;
}
.importPageQueueClear:hover {
color: #fff;
}
.importPageQueueList { .importPageQueueList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -956,11 +840,6 @@
min-width: 0; min-width: 0;
} }
.importPageMatchUnmatch {
grid-area: unmatch;
justify-self: end;
}
.importPageSinglesHeader { .importPageSinglesHeader {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@ -1097,7 +976,6 @@
.autoImportCard:hover { .autoImportCard:hover {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
} }
.autoImportCompleted { .autoImportCompleted {
@ -1214,27 +1092,6 @@
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
} }
/* Scan Now button */
.autoImportScanNowBtn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
padding: 4px 12px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.15s;
margin-left: auto;
}
.autoImportScanNowBtn:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
/* Live progress */ /* Live progress */
.autoImportProgress { .autoImportProgress {
margin-top: 8px; margin-top: 8px;
@ -1288,105 +1145,10 @@
/* Filter pills */ /* Filter pills */
.autoImportFilters { .autoImportFilters {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 0; padding: 6px 0;
margin-bottom: 8px; margin-bottom: 8px;
} }
.autoImportFilterPill {
padding: 6px 16px;
border-radius: 8px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.45);
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.autoImportFilterPill:hover {
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.7);
}
.autoImportFilterPill.active {
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-light-rgb));
box-shadow: 0 2px 8px rgba(var(--accent-rgb), 0.15);
}
.autoImportActionBtn {
display: inline-flex;
align-items: center;
gap: 7px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
letter-spacing: 0.1px;
}
.autoImportActionPrimary {
background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgba(var(--accent-rgb), 0.85));
color: #fff;
border-color: rgba(var(--accent-rgb), 0.3);
box-shadow: 0 2px 8px rgba(var(--accent-rgb), 0.2);
}
.autoImportActionPrimary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(var(--accent-rgb), 0.3);
filter: brightness(1.1);
}
.autoImportActionPrimary:disabled {
opacity: 0.45;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.autoImportActionSecondary {
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.7);
border-color: rgba(255, 255, 255, 0.08);
}
.autoImportActionSecondary:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
/* Batch action buttons */
.autoImportBatchBtn {
font-size: 11px;
padding: 4px 12px;
border-radius: 6px;
border: 1px solid rgba(var(--accent-rgb), 0.3);
background: rgba(var(--accent-rgb), 0.08);
color: rgba(var(--accent-rgb), 1);
cursor: pointer;
transition: all 0.15s;
}
.autoImportBatchBtn:hover {
background: rgba(var(--accent-rgb), 0.15);
}
.autoImportClearBtn {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.5);
}
.autoImportClearBtn:hover {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.8);
}
.autoImportCardMeta { .autoImportCardMeta {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -1541,11 +1303,6 @@
margin-top: 4px; margin-top: 4px;
} }
.autoImportActions button {
font-size: 10px;
padding: 3px 10px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.autoImportCard { .autoImportCard {
flex-direction: column; flex-direction: column;

View file

@ -79,9 +79,7 @@ function ImportHeader({
</h1> </h1>
<Button <Button
type="button" type="button"
className={`${styles.importPageRefreshBtn} ${ variant="secondary"
refreshing ? styles.importPageRefreshBtnRefreshing : ''
}`}
title="Re-scan import folder" title="Re-scan import folder"
aria-busy={refreshing} aria-busy={refreshing}
disabled={refreshing} disabled={refreshing}
@ -121,7 +119,8 @@ function ImportProcessingQueue() {
<span className={styles.importPageQueueTitle}>Processing</span> <span className={styles.importPageQueueTitle}>Processing</span>
<Button <Button
type="button" type="button"
className={styles.importPageQueueClear} variant="ghost"
size="sm"
id="import-page-queue-clear" id="import-page-queue-clear"
style={{ display: hasFinished ? undefined : 'none' }} style={{ display: hasFinished ? undefined : 'none' }}
onClick={clearFinishedJobs} onClick={clearFinishedJobs}

View file

@ -169,13 +169,13 @@ export function SinglesImportPanel({
}) { }) {
const selectedCount = files.filter((file) => selected.has(getStagingFileKey(file))).length; const selectedCount = files.filter((file) => selected.has(getStagingFileKey(file))).length;
const allSelected = files.length > 0 && selectedCount === files.length; const allSelected = files.length > 0 && selectedCount === files.length;
const processVariant = selectedCount > 0 ? 'primary' : 'default'; const processVariant = selectedCount > 0 ? 'primary' : 'secondary';
return ( return (
<> <>
<div className={styles.importPageSinglesHeader}> <div className={styles.importPageSinglesHeader}>
<div className={styles.importPageSinglesActions}> <div className={styles.importPageSinglesActions}>
<Button type="button" className={styles.importPageSecondaryBtn} onClick={onSelectAll}> <Button type="button" variant="secondary" size="sm" onClick={onSelectAll}>
<span id="import-page-select-all-text"> <span id="import-page-select-all-text">
{allSelected ? 'Deselect All' : 'Select All'} {allSelected ? 'Deselect All' : 'Select All'}
</span> </span>
@ -236,13 +236,14 @@ export function SinglesImportPanel({
) : null} ) : null}
</div> </div>
<div className={styles.importPageSingleActions}> <div className={styles.importPageSingleActions}>
<button <Button
type="button" type="button"
className={styles.importPageIdentifyBtn} variant="secondary"
size="sm"
onClick={() => onOpenSearch(file)} onClick={() => onOpenSearch(file)}
> >
🔍 Identify 🔍 Identify
</button> </Button>
</div> </div>
{openSearchKey === fileKey ? ( {openSearchKey === fileKey ? (
<SingleSearchPanel <SingleSearchPanel
@ -292,7 +293,8 @@ function SingleSearchPanel({
/> />
<Button <Button
type="button" type="button"
className={styles.importPageSingleSearchGo} variant="primary"
size="sm"
onClick={() => onRunSearch(fileKey, query)} onClick={() => onRunSearch(fileKey, query)}
> >
Search Search