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;
|
||||
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…
Reference in a new issue