feat: add Button component to shared/ui
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { Button } from './ui/Button'
|
||||
export type { ButtonVariant, ButtonSize } from './ui/Button'
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Button } from './Button'
|
||||
|
||||
describe('Button', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders children', () => {
|
||||
render(<Button>Click me</Button>)
|
||||
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument()
|
||||
})
|
||||
it('renders as button element', () => {
|
||||
render(<Button>Click</Button>)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
describe('variants', () => {
|
||||
it('applies primary variant by default', () => {
|
||||
render(<Button>Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('bg-burnt-oxide')
|
||||
})
|
||||
it('applies secondary variant', () => {
|
||||
render(<Button variant="secondary">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('bg-slate-indigo')
|
||||
})
|
||||
it('applies outline variant', () => {
|
||||
render(<Button variant="outline">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('bg-transparent')
|
||||
})
|
||||
it('applies ghost variant', () => {
|
||||
render(<Button variant="ghost">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('bg-ochre-clay')
|
||||
})
|
||||
})
|
||||
describe('sizes', () => {
|
||||
it('applies md size by default', () => {
|
||||
render(<Button>Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('px-6', 'py-3')
|
||||
})
|
||||
it('applies sm size', () => {
|
||||
render(<Button size="sm">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('px-4', 'py-2')
|
||||
})
|
||||
it('applies lg size', () => {
|
||||
render(<Button size="lg">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('px-8', 'py-4')
|
||||
})
|
||||
})
|
||||
describe('interactions', () => {
|
||||
it('calls onClick when clicked', async () => {
|
||||
const onClick = vi.fn()
|
||||
render(<Button onClick={onClick}>Go</Button>)
|
||||
await userEvent.click(screen.getByRole('button'))
|
||||
expect(onClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
it('is disabled when disabled prop is set', () => {
|
||||
render(<Button disabled>Go</Button>)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
describe('className passthrough', () => {
|
||||
it('merges custom className', () => {
|
||||
render(<Button className="w-full">Go</Button>)
|
||||
expect(screen.getByRole('button')).toHaveClass('w-full')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
||||
import { cn } from '$shared/lib'
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* Visual variant
|
||||
* @default 'primary'
|
||||
*/
|
||||
variant?: ButtonVariant
|
||||
/**
|
||||
* Size preset
|
||||
* @default 'md'
|
||||
*/
|
||||
size?: ButtonSize
|
||||
/**
|
||||
* Button content
|
||||
*/
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const VARIANTS: Record<ButtonVariant, string> = {
|
||||
primary: 'bg-burnt-oxide text-ochre-clay',
|
||||
secondary: 'bg-slate-indigo text-ochre-clay',
|
||||
outline: 'bg-transparent text-carbon-black border-carbon-black',
|
||||
ghost: 'bg-ochre-clay text-carbon-black border-carbon-black',
|
||||
}
|
||||
|
||||
const SIZES: Record<ButtonSize, string> = {
|
||||
sm: 'px-4 py-2 text-sm',
|
||||
md: 'px-6 py-3 text-base',
|
||||
lg: 'px-8 py-4 text-lg',
|
||||
}
|
||||
|
||||
const BASE = 'brutal-border brutal-shadow transition-all duration-200 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[6px_6px_0_var(--carbon-black)] active:translate-x-[4px] active:translate-y-[4px] active:shadow-[4px_4px_0_var(--carbon-black)] uppercase tracking-wider'
|
||||
|
||||
/**
|
||||
* Brutalist button with variants and sizes.
|
||||
*/
|
||||
export function Button({ variant = 'primary', size = 'md', className, children, ...props }: Props) {
|
||||
return (
|
||||
<button className={cn(BASE, VARIANTS[variant], SIZES[size], className)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user