Component Development Guidelines
Component Development Guidelines
Component Structure
// components/Button/Button.tsx
import React from 'react';
import { cn } from '@/lib/utils';
import './Button.css';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'tertiary';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
fullWidth?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
className,
variant = 'primary',
size = 'md',
loading = false,
fullWidth = false,
disabled,
children,
...props
}, ref) => {
return (
<button
ref={ref}
className={cn(
'button',
`button--${variant}`,
`button--${size}`,
{
'button--loading': loading,
'button--full-width': fullWidth,
'button--disabled': disabled || loading
},
className
)}
disabled={disabled || loading}
{...props}
>
{loading && <Spinner size="sm" />}
<span className="button__content">{children}</span>
</button>
);
});
Button.displayName = 'Button';
Component Testing
// components/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with default props', () => {
render(<Button>Click me<);
expect(screen.getByRole('button', { name: /click me)).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me<);
fireEvent.click(screen.getByRole('button', { name: /click me));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Submit<);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});
Component Documentation
// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: 'A versatile button component with multiple variants and states.'
}
}
},
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'tertiary'],
description: 'The visual style of the button'
},
size: {
control: { type: 'select' },
options: ['sm', 'md', 'lg'],
description: 'The size of the button'
},
loading: {
control: 'boolean',
description: 'Shows a loading spinner and disables the button'
}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button'
}
};
export const Loading: Story = {
args: {
variant: 'primary',
loading: true,
children: 'Loading...'
}
};