Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/client/src/components/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,38 @@ describe('Button', () => {
expect(destructive).toContain('theme-destructive-soft:hover:active:bg-surface-destructive/17');
});

it('rests a quiet button in the secondary ink and raises it to the primary one on hover', () => {
const quiet = cn(buttonVariants({ variant: 'quiet' }));
const ghost = cn(buttonVariants({ variant: 'ghost' }));

expect(quiet).toContain('text-text-secondary');
expect(quiet).toContain('hover:text-text-primary');
expect(quiet).toContain('hover:bg-surface-hover');
expect(ghost).not.toContain('text-text-secondary');
});

it('offers the message, link, carousel and toolbar roles as variants', () => {
expect(cn(buttonVariants({ variant: 'message-action' }))).toContain('text-text-secondary-alt');
expect(cn(buttonVariants({ variant: 'message-action' }))).toContain('p-1.5');
expect(cn(buttonVariants({ variant: 'inline-link' }))).toContain('hover:bg-transparent');
expect(cn(buttonVariants({ variant: 'inline-link' }))).toContain('focus-visible:ring-offset-0');
expect(cn(buttonVariants({ variant: 'carousel-nav' }))).toContain('bg-surface-fixed');
expect(cn(buttonVariants({ variant: 'carousel-nav' }))).toContain(
'hover:bg-surface-fixed-hover',
);
expect(cn(buttonVariants({ variant: 'toolbar' }))).toContain('text-xs');
expect(cn(buttonVariants({ variant: 'toolbar' }))).toContain('hover:bg-surface-hover');
});

it('lets the message, link, carousel and toolbar roles keep their own box at the default size', () => {
expect(cn(buttonVariants({ variant: 'message-action' }))).not.toContain('px-4');
expect(cn(buttonVariants({ variant: 'inline-link' }))).not.toContain('h-theme-button');
expect(cn(buttonVariants({ variant: 'carousel-nav' }))).toContain('p-2');
expect(cn(buttonVariants({ variant: 'carousel-nav' }))).not.toContain('px-4');
expect(cn(buttonVariants({ variant: 'toolbar' }))).toContain('py-1');
expect(cn(buttonVariants({ variant: 'toolbar' }))).not.toContain('py-2');
});

it('outlines a toggle in the control border', () => {
expect(cn(buttonVariants({ variant: 'outline-toggle' }))).toContain('border-border-control');
});
Expand Down
27 changes: 27 additions & 0 deletions packages/client/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type ButtonVariantOptions =
| 'destructive'
| 'secondary'
| 'ghost'
| 'quiet'
| 'message-action'
| 'inline-link'
| 'carousel-nav'
| 'toolbar'
| 'media'
| 'row-action'
| 'row-action-reveal'
Expand Down Expand Up @@ -87,6 +92,23 @@ const buttonVariantRecipe = cva(
secondary:
'bg-surface-secondary text-text-primary hover:bg-surface-hover hover:active:bg-surface-pressed',
ghost: 'hover:bg-surface-hover hover:active:bg-surface-pressed hover:text-text-primary',
/** A ghost that rests in the secondary ink and rises to the primary one under the pointer,
* for a control that should not compete with the content it sits beside. */
quiet:
'text-text-secondary hover:bg-surface-hover hover:active:bg-surface-pressed hover:text-text-primary',
/** An icon action under a message: a small padded square that rests in the alt secondary ink. */
'message-action':
'size-auto rounded-lg p-1.5 text-text-secondary-alt hover:bg-surface-hover hover:text-text-primary',
/** A text action that reads as a link in a list or footer: no fill at rest or under the pointer,
* and a ring flush against the control. */
'inline-link':
'h-auto justify-start gap-2 rounded-none p-0 font-normal text-text-secondary hover:bg-transparent hover:text-text-primary focus-visible:ring-offset-0',
/** A previous or next arrow floating over a carousel's content, on the fixed surface. */
'carousel-nav':
'rounded-xl bg-surface-fixed p-2 text-text-fixed shadow-lg hover:bg-surface-fixed-hover hover:shadow-xl',
/** A compact text action in a toolbar, quiet until hovered. */
toolbar:
'rounded-sm bg-transparent px-2 py-1 text-xs font-normal text-text-secondary hover:bg-surface-hover',
/**
* A control drawn over the user's own media (a lightbox toolbar, an image preview's close):
* ghost-shaped, with the media ink and a tint of it on hover, so it stays legible on the
Expand Down Expand Up @@ -248,6 +270,11 @@ const buttonVariantRecipe = cva(
size: 'default',
class: 'h-auto px-1 py-2',
},
/* These carry their own box, which the default size's height and padding would otherwise win. */
{ variant: 'message-action', size: 'default', class: 'size-auto p-1.5' },
{ variant: 'inline-link', size: 'default', class: 'h-auto p-0' },
{ variant: 'carousel-nav', size: 'default', class: 'h-auto p-2' },
{ variant: 'toolbar', size: 'default', class: 'h-auto px-2 py-1' },
/* Sized by its own label, so a long option wraps instead of clipping. */
{
variant: 'option',
Expand Down
Loading