refactor(webui): theme import buttons
- add shared button variants and size controls backed by theme-aware styles - move album import search controls onto shared button styling - keep the import route layout-specific CSS limited to positioning
This commit is contained in:
parent
0d7fb91d98
commit
af3d51c2ed
6 changed files with 115 additions and 77 deletions
|
|
@ -363,11 +363,8 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
min-height: 36px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
transition:
|
transition:
|
||||||
transform 0.18s ease,
|
transform 0.18s ease,
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
|
|
@ -376,6 +373,34 @@
|
||||||
color 0.18s ease;
|
color 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonSizeSm {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSizeMd {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSizeLg {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonIcon {
|
||||||
|
width: var(--button-icon-size, 36px);
|
||||||
|
height: var(--button-icon-size, 36px);
|
||||||
|
min-height: var(--button-icon-size, 36px);
|
||||||
|
min-width: var(--button-icon-size, 36px);
|
||||||
|
padding: 0;
|
||||||
|
font-size: var(--button-icon-font-size, 15px);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.button:hover:not(:disabled) {
|
.button:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
border-color: rgba(255, 255, 255, 0.18);
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
|
@ -394,6 +419,51 @@
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonPrimary {
|
||||||
|
border-color: rgba(var(--accent-light-rgb), 0.45);
|
||||||
|
background: rgb(var(--accent-light-rgb));
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonPrimary:hover:not(:disabled) {
|
||||||
|
border-color: rgba(var(--accent-light-rgb), 0.55);
|
||||||
|
background: rgba(var(--accent-light-rgb), 0.95);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonPrimary:focus-visible {
|
||||||
|
border-color: rgba(var(--accent-light-rgb), 0.8);
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonPrimary:disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
background: rgba(var(--accent-light-rgb), 0.45);
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGhost {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGhost:hover:not(:disabled) {
|
||||||
|
transform: none;
|
||||||
|
border-color: rgba(255, 255, 255, 0.14);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGhost:focus-visible {
|
||||||
|
border-color: rgba(var(--accent-light-rgb), 0.42);
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGhost:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.formError {
|
.formError {
|
||||||
color: #ff8d8d;
|
color: #ff8d8d;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,9 @@ function FormDemo() {
|
||||||
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button type="button">Cancel</Button>
|
<Button type="button">Cancel</Button>
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit" variant="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
@ -154,6 +156,9 @@ describe('form primitives', () => {
|
||||||
expect(highPriority).toHaveAttribute('aria-pressed', 'true');
|
expect(highPriority).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute(
|
||||||
|
'data-variant',
|
||||||
|
'primary',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -247,13 +247,35 @@ 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';
|
||||||
|
variant?: 'default' | 'primary' | 'ghost';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||||
{ className, type = 'button', ...props },
|
{ className, size = 'md', type = 'button', variant = 'default', ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return <BaseButton ref={ref} className={clsx(styles.button, className)} type={type} {...props} />;
|
return (
|
||||||
|
<BaseButton
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(
|
||||||
|
styles.button,
|
||||||
|
{
|
||||||
|
[styles.buttonSizeSm]: size === 'sm',
|
||||||
|
[styles.buttonSizeMd]: size === 'md',
|
||||||
|
[styles.buttonSizeLg]: size === 'lg',
|
||||||
|
[styles.buttonIcon]: size === 'icon',
|
||||||
|
[styles.buttonPrimary]: variant === 'primary',
|
||||||
|
[styles.buttonGhost]: variant === 'ghost',
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
type={type}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function FormError({ className, message }: { className?: string; message?: ReactNode }) {
|
export function FormError({ className, message }: { className?: string; message?: ReactNode }) {
|
||||||
|
|
|
||||||
|
|
@ -298,18 +298,19 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
|
||||||
if (event.key === 'Enter') onRunSearch();
|
if (event.key === 'Enter') onRunSearch();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button type="button" className={styles.importPageSearchBtn} onClick={onRunSearch}>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.importPageClearBtn} ${albumResults === null ? styles.hidden : ''}`}
|
variant="ghost"
|
||||||
|
className={albumResults === null ? styles.hidden : ''}
|
||||||
id="import-page-album-clear-btn"
|
id="import-page-album-clear-btn"
|
||||||
title="Clear search"
|
title="Clear search"
|
||||||
onClick={onBackToSearch}
|
onClick={onBackToSearch}
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="button" variant="primary" onClick={onRunSearch}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.importPageAlbumGrid} id="import-page-album-results">
|
<div className={styles.importPageAlbumGrid} id="import-page-album-results">
|
||||||
|
|
@ -568,7 +569,7 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.importPageProcessBtn}
|
variant="primary"
|
||||||
id="import-page-album-process-btn"
|
id="import-page-album-process-btn"
|
||||||
disabled={matchedCount === 0}
|
disabled={matchedCount === 0}
|
||||||
onClick={onProcessAlbum}
|
onClick={onProcessAlbum}
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.importPageSearchInput {
|
.importPageSearchInput {
|
||||||
|
|
@ -176,38 +175,10 @@
|
||||||
border-color: rgba(var(--accent-light-rgb), 0.5);
|
border-color: rgba(var(--accent-light-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.importPageSearchBtn {
|
.importPageSearchBar > button {
|
||||||
padding: 10px 20px;
|
align-self: stretch;
|
||||||
background: rgb(var(--accent-light-rgb));
|
min-width: 40px;
|
||||||
border: none;
|
padding: 0 12px;
|
||||||
border-radius: 10px;
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageSearchBtn:hover {
|
|
||||||
background: #1fdf64;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageClearBtn {
|
|
||||||
position: absolute;
|
|
||||||
right: 100px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageClearBtn:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Album grid */
|
/* Album grid */
|
||||||
|
|
@ -537,37 +508,10 @@
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.importPageProcessBtn {
|
|
||||||
padding: 10px 24px;
|
|
||||||
background: rgb(var(--accent-light-rgb));
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageProcessBtn:hover {
|
|
||||||
background: #1fdf64;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageProcessBtn:disabled {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importPageSecondaryBtn {
|
.importPageSecondaryBtn {
|
||||||
padding: 8px 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-radius: 8px;
|
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,13 +521,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.importPageBackBtn {
|
.importPageBackBtn {
|
||||||
padding: 8px 16px;
|
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ export function SinglesImportPanel({
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.importPageProcessBtn}
|
variant="primary"
|
||||||
id="import-page-singles-process-btn"
|
id="import-page-singles-process-btn"
|
||||||
disabled={selectedCount === 0}
|
disabled={selectedCount === 0}
|
||||||
onClick={onProcessSingles}
|
onClick={onProcessSingles}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue