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);
|
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'] {
|
&[data-selected='true'] {
|
||||||
|
color: #fff;
|
||||||
border-color: rgba(var(--accent-light-rgb), 0.5);
|
border-color: rgba(var(--accent-light-rgb), 0.5);
|
||||||
background: rgba(var(--accent-rgb), 0.18);
|
background: rgba(var(--accent-rgb), 0.18);
|
||||||
box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08);
|
box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08);
|
||||||
|
|
@ -598,7 +612,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-variant='ghost']:hover:not(:disabled) {
|
&[data-variant='ghost']:hover:not(:disabled) {
|
||||||
transform: none;
|
|
||||||
border-color: rgba(255, 255, 255, 0.14);
|
border-color: rgba(255, 255, 255, 0.14);
|
||||||
background: rgba(255, 255, 255, 0.07);
|
background: rgba(255, 255, 255, 0.07);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
@ -610,7 +623,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-variant='ghost']:disabled {
|
&[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.getByText('12')).toHaveAttribute('data-tone', 'warning');
|
||||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute(
|
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('data-variant', 'primary');
|
||||||
'data-variant',
|
|
||||||
'primary',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports compact option button groups', () => {
|
it('supports compact option button groups', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<OptionButtonGroup size="sm">
|
<OptionButtonGroup size="sm">
|
||||||
<OptionButton selected>All</OptionButton>
|
<OptionButton selected>All</OptionButton>
|
||||||
<OptionButton>Pending</OptionButton>
|
<OptionButton variant="ghost">Pending</OptionButton>
|
||||||
</OptionButtonGroup>,
|
</OptionButtonGroup>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container.querySelector('[data-size="sm"]')).toBeInTheDocument();
|
expect(container.querySelector('[data-size="sm"]')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Pending' })).toHaveAttribute(
|
||||||
|
'data-variant',
|
||||||
|
'ghost',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports compact select sizing', () => {
|
it('supports compact select sizing', () => {
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<select
|
<select ref={ref} className={clsx(styles.select, className)} data-size={size} {...props} />
|
||||||
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.Control className={styles.rangeControl}>
|
||||||
<Slider.Track className={styles.rangeTrack}>
|
<Slider.Track className={styles.rangeTrack}>
|
||||||
<Slider.Indicator className={styles.rangeIndicator} />
|
<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.Track>
|
||||||
</Slider.Control>
|
</Slider.Control>
|
||||||
</Slider.Root>
|
</Slider.Root>
|
||||||
|
|
@ -249,11 +247,7 @@ export interface OptionButtonGroupProps {
|
||||||
size?: OptionButtonGroupSize;
|
size?: OptionButtonGroupSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OptionButtonGroup({
|
export function OptionButtonGroup({ className, children, size = 'md' }: OptionButtonGroupProps) {
|
||||||
className,
|
|
||||||
children,
|
|
||||||
size = 'md',
|
|
||||||
}: OptionButtonGroupProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.optionButtonGroup, className)} data-size={size}>
|
<div className={clsx(styles.optionButtonGroup, className)} data-size={size}>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -261,14 +255,17 @@ export function OptionButtonGroup({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OptionButtonVariant = 'default' | 'ghost';
|
||||||
|
|
||||||
export interface OptionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
|
export interface OptionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
variant?: OptionButtonVariant;
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(function OptionButton(
|
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,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -277,6 +274,7 @@ export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(fun
|
||||||
pressed={selected}
|
pressed={selected}
|
||||||
className={clsx(styles.optionButton, className)}
|
className={clsx(styles.optionButton, className)}
|
||||||
data-selected={selected ? 'true' : undefined}
|
data-selected={selected ? 'true' : undefined}
|
||||||
|
data-variant={variant}
|
||||||
type={type}
|
type={type}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -298,14 +296,7 @@ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
|
||||||
{ className, tone = 'neutral', ...props },
|
{ className, tone = 'neutral', ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return <span ref={ref} className={clsx(styles.badge, className)} data-tone={tone} {...props} />;
|
||||||
<span
|
|
||||||
ref={ref}
|
|
||||||
className={clsx(styles.badge, className)}
|
|
||||||
data-tone={tone}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;
|
type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,7 @@ export function AutoImportPanel({
|
||||||
<OptionButton
|
<OptionButton
|
||||||
key={filter}
|
key={filter}
|
||||||
selected={autoFilter === filter}
|
selected={autoFilter === filter}
|
||||||
|
variant={autoFilter === filter ? 'default' : 'ghost'}
|
||||||
onClick={() => onFilterChange(filter)}
|
onClick={() => onFilterChange(filter)}
|
||||||
>
|
>
|
||||||
<span>{getAutoImportFilterLabel(filter)}</span>
|
<span>{getAutoImportFilterLabel(filter)}</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue