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
pull/686/head
Antti Kettunen 1 week ago
parent 0d7fb91d98
commit af3d51c2ed
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

@ -363,11 +363,8 @@
color: #fff;
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
min-height: 36px;
padding: 8px 12px;
transition:
transform 0.18s ease,
border-color 0.18s ease,
@ -376,6 +373,34 @@
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) {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.18);
@ -394,6 +419,51 @@
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 {
color: #ff8d8d;
font-size: 12px;

@ -112,7 +112,9 @@ function FormDemo() {
<FormActions>
<Button type="button">Cancel</Button>
<Button type="submit">Save</Button>
<Button type="submit" variant="primary">
Save
</Button>
</FormActions>
</form>
);
@ -154,6 +156,9 @@ describe('form primitives', () => {
expect(highPriority).toHaveAttribute('aria-pressed', 'true');
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'> & {
className?: string;
size?: 'sm' | 'md' | 'lg' | 'icon';
variant?: 'default' | 'primary' | 'ghost';
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, type = 'button', ...props },
{ className, size = 'md', type = 'button', variant = 'default', ...props },
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 }) {

@ -298,18 +298,19 @@ function AlbumImportPanelContent({ viewModel }: { viewModel: AlbumImportViewMode
if (event.key === 'Enter') onRunSearch();
}}
/>
<Button type="button" className={styles.importPageSearchBtn} onClick={onRunSearch}>
Search
</Button>
<Button
type="button"
className={`${styles.importPageClearBtn} ${albumResults === null ? styles.hidden : ''}`}
variant="ghost"
className={albumResults === null ? styles.hidden : ''}
id="import-page-album-clear-btn"
title="Clear search"
onClick={onBackToSearch}
>
x
</Button>
<Button type="button" variant="primary" onClick={onRunSearch}>
Search
</Button>
</div>
<div className={styles.importPageAlbumGrid} id="import-page-album-results">
@ -568,7 +569,7 @@ function AlbumMatchPanel({ viewModel }: { viewModel: AlbumImportViewModel }) {
</div>
<Button
type="button"
className={styles.importPageProcessBtn}
variant="primary"
id="import-page-album-process-btn"
disabled={matchedCount === 0}
onClick={onProcessAlbum}

@ -157,7 +157,6 @@
display: flex;
gap: 8px;
margin-bottom: 20px;
position: relative;
}
.importPageSearchInput {
@ -176,38 +175,10 @@
border-color: rgba(var(--accent-light-rgb), 0.5);
}
.importPageSearchBtn {
padding: 10px 20px;
background: rgb(var(--accent-light-rgb));
border: none;
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;
.importPageSearchBar > button {
align-self: stretch;
min-width: 40px;
padding: 0 12px;
}
/* Album grid */
@ -537,37 +508,10 @@
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 {
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;
}
@ -577,13 +521,9 @@
}
.importPageBackBtn {
padding: 8px 16px;
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: rgba(255, 255, 255, 0.5);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}

@ -181,7 +181,7 @@ export function SinglesImportPanel({
</Button>
<Button
type="button"
className={styles.importPageProcessBtn}
variant="primary"
id="import-page-singles-process-btn"
disabled={selectedCount === 0}
onClick={onProcessSingles}

Loading…
Cancel
Save