feat(form): add compact option groups

- add a size prop to OptionButtonGroup with a denser sm layout\n- use the compact filter group on the auto-import panel\n- keep the new size variants covered in form and route tests
pull/686/head
Antti Kettunen 1 week ago
parent fccc03efef
commit 9ba54bd82d
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

@ -396,6 +396,17 @@
gap: 8px;
}
.optionButtonGroupSizeSm {
gap: 6px;
}
.optionButtonGroupSizeSm .optionButton {
min-width: 0;
padding: 6px 12px;
font-size: 12px;
gap: 6px;
}
.optionButton {
display: inline-flex;
align-items: center;

@ -178,6 +178,17 @@ describe('form primitives', () => {
);
});
it('supports compact option button groups', () => {
const { container } = render(
<OptionButtonGroup size="sm">
<OptionButton selected>All</OptionButton>
<OptionButton>Pending</OptionButton>
</OptionButtonGroup>,
);
expect(container.querySelector('[data-size="sm"]')).toBeInTheDocument();
});
it('supports compact select sizing', () => {
render(
<Select aria-label="Compact" defaultValue="one" size="sm">

@ -247,14 +247,33 @@ export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(functio
);
});
export type OptionButtonGroupSize = 'sm' | 'md';
export interface OptionButtonGroupProps {
children: ReactNode;
className?: string;
size?: OptionButtonGroupSize;
}
export function OptionButtonGroup({
className,
children,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={clsx(styles.optionButtonGroup, className)}>{children}</div>;
size = 'md',
}: OptionButtonGroupProps) {
return (
<div
className={clsx(
styles.optionButtonGroup,
{
[styles.optionButtonGroupSizeSm]: size === 'sm',
},
className,
)}
data-size={size}
>
{children}
</div>
);
}
export interface OptionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {

@ -304,6 +304,8 @@ describe('import route', () => {
expect(await screen.findByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
expect(screen.getByText('Watching')).toHaveAttribute('data-tone', 'success');
const compactFilterGroup = document.querySelector('#auto-import-results [data-size="sm"]');
expect(compactFilterGroup).toBeInTheDocument();
const intervalSelect = document.getElementById('auto-import-interval');
if (!(intervalSelect instanceof HTMLElement)) {
throw new Error('auto-import interval select missing');

@ -255,43 +255,45 @@ export function AutoImportPanel({
</div>
{allResults.length > 0 ? (
<>
<OptionButtonGroup className={styles.autoImportFilters}>
{(['all', 'pending', 'imported', 'failed'] as const).map((filter) => (
<OptionButton key={filter} selected={autoFilter === filter} onClick={() => onFilterChange(filter)}>
<span>{getAutoImportFilterLabel(filter)}</span>
<Badge tone={getAutoImportFilterTone(filter)}>
{getAutoImportFilterCount(filter, counts, allResults.length)}
</Badge>
</OptionButton>
))}
<div className={styles.importPageFlexSpacer} />
{counts.review > 0 ? (
<Button
type="button"
variant="secondary"
size="sm"
id="auto-import-approve-all"
disabled={approveAllMutation.isPending}
onClick={() => approveAllMutation.mutate()}
>
Approve All
</Button>
) : null}
{counts.imported + counts.failed > 0 ? (
<Button
type="button"
variant="ghost"
size="sm"
id="auto-import-clear-completed"
disabled={clearMutation.isPending}
onClick={() => clearMutation.mutate()}
>
Clear History
</Button>
) : null}
</OptionButtonGroup>
</>
<OptionButtonGroup size="sm" className={styles.autoImportFilters}>
{(['all', 'pending', 'imported', 'failed'] as const).map((filter) => (
<OptionButton
key={filter}
selected={autoFilter === filter}
onClick={() => onFilterChange(filter)}
>
<span>{getAutoImportFilterLabel(filter)}</span>
<Badge tone={getAutoImportFilterTone(filter)}>
{getAutoImportFilterCount(filter, counts, allResults.length)}
</Badge>
</OptionButton>
))}
<div className={styles.importPageFlexSpacer} />
{counts.review > 0 ? (
<Button
type="button"
variant="secondary"
size="sm"
id="auto-import-approve-all"
disabled={approveAllMutation.isPending}
onClick={() => approveAllMutation.mutate()}
>
Approve All
</Button>
) : null}
{counts.imported + counts.failed > 0 ? (
<Button
type="button"
variant="ghost"
size="sm"
id="auto-import-clear-completed"
disabled={clearMutation.isPending}
onClick={() => clearMutation.mutate()}
>
Clear History
</Button>
) : null}
</OptionButtonGroup>
) : null}
<div className={styles.autoImportResults} id="auto-import-results">
@ -354,8 +356,8 @@ function AutoImportResultCard({
status: ImportAutoImportStatusPayload | undefined;
onApprove: () => void;
onReject: () => void;
onToggle: () => void;
}) {
onToggle: () => void;
}) {
const confidencePercent = Math.round((result.confidence || 0) * 100);
const confidenceClass = getConfidenceClass(confidencePercent);
const statusMeta = getAutoImportStatusMeta(result.status);
@ -412,11 +414,17 @@ function AutoImportResultCard({
)}
</div>
<div className={styles.autoImportCardCenter}>
<div className={styles.autoImportCardAlbum}>{result.album_name || result.folder_name}</div>
<div className={styles.autoImportCardArtist}>{result.artist_name || 'Unknown Artist'}</div>
<div className={styles.autoImportCardAlbum}>
{result.album_name || result.folder_name}
</div>
<div className={styles.autoImportCardArtist}>
{result.artist_name || 'Unknown Artist'}
</div>
<div className={styles.autoImportCardMeta}>
<span>{matchSummary}</span>
{methodLabel ? <span className={styles.autoImportMethodBadge}>{methodLabel}</span> : null}
{methodLabel ? (
<span className={styles.autoImportMethodBadge}>{methodLabel}</span>
) : null}
{timeAgo ? <span>{timeAgo}</span> : null}
</div>
{result.error_message ? (
@ -488,13 +496,12 @@ function AutoImportResultCard({
.filter(Boolean)
.join(' ');
return (
<div
key={`${track.name}-${track.file}-${trackIndex}`}
className={rowClassName}
>
<div key={`${track.name}-${track.file}-${trackIndex}`} className={rowClassName}>
<span className={styles.autoImportTrackName}>{track.name}</span>
<span className={styles.autoImportTrackFile}>{track.file}</span>
<span className={`${styles.autoImportTrackConf} ${getAutoImportConfidenceClass(getConfidenceClass(track.confidence))}`}>
<span
className={`${styles.autoImportTrackConf} ${getAutoImportConfidenceClass(getConfidenceClass(track.confidence))}`}
>
{track.confidence}%
</span>
</div>
@ -573,7 +580,9 @@ function getAutoImportFilterCount(
}
}
function getAutoImportFilterTone(filter: ImportAutoFilter): 'neutral' | 'warning' | 'success' | 'danger' {
function getAutoImportFilterTone(
filter: ImportAutoFilter,
): 'neutral' | 'warning' | 'success' | 'danger' {
switch (filter) {
case 'pending':
return 'warning';

Loading…
Cancel
Save