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