feat(form): add ghost option variant

- Keep option buttons transparent by default and subtle on hover
- Use the ghost style for inactive auto-import filters so the active one stands out
- Keep OptionButton aligned with the existing button variant API
This commit is contained in:
Antti Kettunen 2026-05-23 22:06:37 +03:00
parent 5a227e3ace
commit 7bd6e0ba09
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
4 changed files with 34 additions and 28 deletions

View file

@ -420,7 +420,21 @@
background: rgba(255, 255, 255, 0.08);
}
&[data-variant='ghost'] {
border-color: transparent;
background: transparent;
color: rgba(255, 255, 255, 0.58);
}
&[data-variant='ghost']:hover:not(:disabled) {
transform: none;
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
}
&[data-selected='true'] {
color: #fff;
border-color: rgba(var(--accent-light-rgb), 0.5);
background: rgba(var(--accent-rgb), 0.18);
box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08);
@ -598,7 +612,6 @@
}
&[data-variant='ghost']:hover:not(:disabled) {
transform: none;
border-color: rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.07);
color: #fff;
@ -610,7 +623,7 @@
}
&[data-variant='ghost']:disabled {
opacity: 0.5;
opacity: 0.55;
}
}

View file

@ -172,21 +172,22 @@ describe('form primitives', () => {
expect(screen.getByText('12')).toHaveAttribute('data-tone', 'warning');
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute(
'data-variant',
'primary',
);
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('data-variant', 'primary');
});
it('supports compact option button groups', () => {
const { container } = render(
<OptionButtonGroup size="sm">
<OptionButton selected>All</OptionButton>
<OptionButton>Pending</OptionButton>
<OptionButton variant="ghost">Pending</OptionButton>
</OptionButtonGroup>,
);
expect(container.querySelector('[data-size="sm"]')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Pending' })).toHaveAttribute(
'data-variant',
'ghost',
);
});
it('supports compact select sizing', () => {

View file

@ -89,12 +89,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
ref,
) {
return (
<select
ref={ref}
className={clsx(styles.select, className)}
data-size={size}
{...props}
/>
<select ref={ref} className={clsx(styles.select, className)} data-size={size} {...props} />
);
});
@ -186,7 +181,10 @@ export const RangeInput = forwardRef<HTMLDivElement, RangeInputProps>(function R
<Slider.Control className={styles.rangeControl}>
<Slider.Track className={styles.rangeTrack}>
<Slider.Indicator className={styles.rangeIndicator} />
<Slider.Thumb aria-label={typeof label === 'string' ? label : undefined} className={styles.rangeThumb} />
<Slider.Thumb
aria-label={typeof label === 'string' ? label : undefined}
className={styles.rangeThumb}
/>
</Slider.Track>
</Slider.Control>
</Slider.Root>
@ -249,11 +247,7 @@ export interface OptionButtonGroupProps {
size?: OptionButtonGroupSize;
}
export function OptionButtonGroup({
className,
children,
size = 'md',
}: OptionButtonGroupProps) {
export function OptionButtonGroup({ className, children, size = 'md' }: OptionButtonGroupProps) {
return (
<div className={clsx(styles.optionButtonGroup, className)} data-size={size}>
{children}
@ -261,14 +255,17 @@ export function OptionButtonGroup({
);
}
export type OptionButtonVariant = 'default' | 'ghost';
export interface OptionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
className?: string;
selected?: boolean;
variant?: OptionButtonVariant;
value?: string;
}
export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(function OptionButton(
{ className, children, selected = false, type = 'button', ...props },
{ className, children, selected = false, type = 'button', variant = 'default', ...props },
ref,
) {
return (
@ -277,6 +274,7 @@ export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(fun
pressed={selected}
className={clsx(styles.optionButton, className)}
data-selected={selected ? 'true' : undefined}
data-variant={variant}
type={type}
{...props}
>
@ -298,14 +296,7 @@ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
{ className, tone = 'neutral', ...props },
ref,
) {
return (
<span
ref={ref}
className={clsx(styles.badge, className)}
data-tone={tone}
{...props}
/>
);
return <span ref={ref} className={clsx(styles.badge, className)} data-tone={tone} {...props} />;
});
type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;

View file

@ -258,6 +258,7 @@ export function AutoImportPanel({
<OptionButton
key={filter}
selected={autoFilter === filter}
variant={autoFilter === filter ? 'default' : 'ghost'}
onClick={() => onFilterChange(filter)}
>
<span>{getAutoImportFilterLabel(filter)}</span>