10 KiB
Component Patterns
This guide documents the component patterns used across the FrenoCorp agent ecosystem.
Architecture Overview
All components follow a consistent pattern:
interface ComponentProps {
// Props with types and defaults
}
/**
* Component description
*
* @param props - Component props
* @returns Rendered element
*/
export function ComponentName(props: ComponentProps) {
// Implementation
}
Standard Patterns
1. Button Components
Pattern: Stateless, prop-driven buttons with variants
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
loading,
disabled,
onClick,
children
}: ButtonProps) {
const buttonClasses = classNames(
'btn',
`btn-${variant}`,
`btn-${size}`,
{ 'btn-disabled': disabled }
);
return <button className={buttonClasses} onClick={onClick} disabled={disabled || loading}>
{loading ? 'Loading...' : children}
</button>;
}
When to use: User actions, form submissions, navigation triggers.
2. Form Inputs
Pattern: Controlled inputs with error handling
interface TextFieldProps {
label?: string;
placeholder?: string;
value: string | number;
onChange: (value: string) => void;
error?: string;
required?: boolean;
disabled?: boolean;
type?: 'text' | 'email' | 'password' | 'number';
}
export function TextField({
label,
placeholder = 'Enter value',
value,
onChange,
error,
required,
disabled,
type = 'text'
}: TextFieldProps) {
return (
<div className="form-group">
{label && <label htmlFor={label}>{label}{required ? '*' : ''}</label>}
<input
id={label}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className={classNames('form-input', { 'form-error': error })}
/>
{error && <span className="error">{error}</span>}
</div>
);
}
When to use: User data entry, search boxes, configuration inputs.
3. Card Components
Pattern: Container with header, body, and optional footer
interface CardProps {
title?: string;
subtitle?: string;
children: React.ReactNode;
actions?: React.ReactNode;
footer?: React.ReactNode;
className?: string;
}
export function Card({
title,
subtitle,
children,
actions,
footer,
className = ''
}: CardProps) {
return (
<div className={`card ${className}`}>
{(title || subtitle) && (
<CardHeader title={title} subtitle={subtitle} />
)}
<CardContent>{children}</CardContent>
{actions && <CardActions>{actions}</CardActions>}
{footer && <CardFooter>{footer}</CardFooter>}
</div>
);
}
When to use: Content containers, data displays, action groups.
4. Tab Components
Pattern: Multi-pane navigation with state management
interface TabsProps {
items: { key: string; label: string }[];
defaultKey?: string;
onChange?: (key: string) => void;
}
export function Tabs({ items, defaultKey = '', onChange }: TabsProps) {
const [activeKey, setActiveKey] = useState(defaultKey);
const handleTabChange = (key: string) => {
onChange?.(key);
setActiveKey(key);
};
return (
<div className="tabs">
<div className="tab-nav">
{items.map(({ key, label }) => (
<button
key={key}
className={`tab ${activeKey === key ? 'active' : ''}`}
onClick={() => handleTabChange(key)}
>
{label}
</button>
))}
</div>
<div className="tab-content">
{/* Tab content */}
</div>
</div>
);
}
When to use: Multi-section views, navigation groups.
5. Modal Components
Pattern: Overlay with centered content and escape handling
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
actions?: React.ReactNode;
}
export function Modal({
isOpen,
onClose,
title,
children,
actions
}: ModalProps) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{title && <ModalTitle>{title}</ModalTitle>}
<ModalContent>{children}</ModalContent>
{actions && <ModalActions>{actions}</ModalActions>}
</div>
</div>
);
}
When to use: Confirmations, forms, detailed views.
Layout Patterns
Page Wrapper
interface PageWrapperProps {
children: React.ReactNode;
title?: string;
breadcrumbs?: string[];
actions?: React.ReactNode;
}
export function PageWrapper({
children,
title,
breadcrumbs = [],
actions
}: PageWrapperProps) {
return (
<div className="page">
{(title || breadcrumbs.length > 0) && (
<PageHeader
title={title}
breadcrumbs={breadcrumbs}
actions={actions}
/>
)}
<main className="page-content">
{children}
</main>
</div>
);
}
Navigation Pane
interface NavigationPaneProps {
children: React.ReactNode;
collapsed?: boolean;
}
export function NavigationPane({
children,
collapsed = false
}: NavigationPaneProps) {
return (
<aside className={`nav-pane ${collapsed ? 'collapsed' : ''}`}>
{children}
</aside>
);
}
Data Display Patterns
Table Component
interface TableProps<T> {
columns: Array<{ key: keyof T; label: string }>;
data: T[];
onRowClick?: (row: T) => void;
emptyMessage?: string;
}
export function Table<T>({
columns,
data,
onRowClick,
emptyMessage = 'No data'
}: TableProps<T>) {
if (data.length === 0) {
return <div className="empty-state">{emptyMessage}</div>;
}
return (
<table>
<thead>
<tr>
{columns.map(({ key, label }) => (
<th key={key}>{label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr
key={String(row[key])}
onClick={() => onRowClick?.(row)}
>
{columns.map(({ key, label }) => (
<td key={key}>{String(row[key])}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Status Badge
interface StatusBadgeProps {
status: 'success' | 'warning' | 'error' | 'info' | 'neutral';
children: React.ReactNode;
}
export function StatusBadge({ status, children }: StatusBadgeProps) {
const styles = {
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800',
neutral: 'bg-gray-100 text-gray-800'
};
return (
<span className={`badge ${styles[status]}`}>
{children}
</span>
);
}
Best Practices
1. Props Order
Always follow this order:
- Boolean flags
- Number values
- String values
- Arrays/Objects
- Children (at the end)
2. Default Values
Provide sensible defaults for all optional props:
interface ButtonProps {
variant?: 'primary' | 'secondary'; // default: 'primary'
size?: 'sm' | 'md' | 'lg'; // default: 'md'
disabled?: boolean; // default: false
}
3. Type Safety
Use discriminated unions for state:
type Status =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T };
| { status: 'error'; error: string };
4. Error Boundaries
Wrap all user-facing components:
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong</div>;
}
return this.props.children;
}
}
5. Accessibility
- Use semantic HTML elements
- Add ARIA labels where needed
- Ensure keyboard navigation works
- Test with screen readers
Testing Guidelines
Every component must have:
- Unit tests for logic and rendering
- Integration tests for prop passing
- Accessibility tests for a11y compliance
Example unit test:
describe('Button', () => {
it('renders children correctly', () => {
const { getByText } = render(<Button>Click me</Button>);
expect(getByText('Click me')).toBeInTheDocument();
});
it('applies correct variant classes', () => {
const { container } = render(<Button variant="secondary">Test</Button>);
expect(container.firstChild).toHaveClass('btn-secondary');
});
});
Adding New Components
When adding a new component:
- Create the component file in
src/components/ - Add TypeScript interface for props
- Write JSDoc with @param and @returns
- Add unit tests
- Update this document if a new pattern emerges
- Ensure it follows existing patterns
Component Naming Conventions
- PascalCase for component names:
Button,TextField - Prefix with functional type:
Card,Modal,Table - Use descriptive names:
UserAvatar, not justIcon - Avoid single-letter components
When to Create a New Component
Create a new component when:
- The same UI pattern appears 3+ times
- The logic is reusable across features
- The component needs its own props interface
Don't create a new component when:
- It's used only once
- It can be composed from existing components
- It's too simple (use HTML elements)
Review Checklist
Before committing component code:
- Props are typed with TypeScript
- JSDoc comments present on exports
- Default values provided for optional props
- Unit tests written
- Accessibility considerations addressed
- Follows existing patterns
- No console.log or debug statements
- Error boundaries where appropriate