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:
parent
5a227e3ace
commit
7bd6e0ba09
4 changed files with 34 additions and 28 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue