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);
}
.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 {
display: inline-flex;
align-items: center;
@ -498,6 +506,29 @@
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 {
border-color: transparent;
background: transparent;

View file

@ -266,7 +266,7 @@ type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;
export type ButtonProps = Omit<BaseButtonProps, 'className'> & {
className?: string;
size?: 'sm' | 'md' | 'lg' | 'icon';
variant?: 'default' | 'primary' | 'ghost';
variant?: 'default' | 'primary' | 'secondary' | 'ghost';
};
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.buttonIcon]: size === 'icon',
[styles.buttonPrimary]: variant === 'primary',
[styles.buttonSecondary]: variant === 'secondary',
[styles.buttonGhost]: variant === 'ghost',
},
className,

View file

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

View file

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

View file

@ -44,30 +44,6 @@
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 {
display: flex;
align-items: center;
@ -175,12 +151,6 @@
border-color: rgba(var(--accent-light-rgb), 0.5);
}
.importPageSearchBar > button {
align-self: stretch;
min-width: 40px;
padding: 0 12px;
}
/* Album grid */
.importPageAlbumGrid {
display: grid;
@ -416,25 +386,6 @@
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 */
.importPageUnmatchedPool {
padding: 16px;
@ -508,30 +459,6 @@
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 */
.importPageSinglesHeader {
display: flex;
@ -631,25 +558,6 @@
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 */
.importPageSingleSearchPanel {
grid-column: 1 / -1;
@ -677,17 +585,6 @@
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 {
display: flex;
flex-direction: column;
@ -796,19 +693,6 @@
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 {
display: flex;
flex-direction: column;
@ -956,11 +840,6 @@
min-width: 0;
}
.importPageMatchUnmatch {
grid-area: unmatch;
justify-self: end;
}
.importPageSinglesHeader {
flex-direction: column;
align-items: flex-start;
@ -1097,7 +976,6 @@
.autoImportCard:hover {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
}
.autoImportCompleted {
@ -1214,27 +1092,6 @@
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 */
.autoImportProgress {
margin-top: 8px;
@ -1288,105 +1145,10 @@
/* Filter pills */
.autoImportFilters {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 0;
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 {
display: flex;
gap: 8px;
@ -1541,11 +1303,6 @@
margin-top: 4px;
}
.autoImportActions button {
font-size: 10px;
padding: 3px 10px;
}
@media (max-width: 768px) {
.autoImportCard {
flex-direction: column;

View file

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

View file

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