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:
parent
b78350a3e2
commit
d066aba03d
7 changed files with 70 additions and 273 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue