updates, fix claude
This commit is contained in:
505
agents/hermes/docs/COMPONENT_PATTERNS.md
Normal file
505
agents/hermes/docs/COMPONENT_PATTERNS.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Component Patterns
|
||||
|
||||
This guide documents the component patterns used across the FrenoCorp agent ecosystem.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
All components follow a consistent pattern:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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:
|
||||
1. Boolean flags
|
||||
2. Number values
|
||||
3. String values
|
||||
4. Arrays/Objects
|
||||
5. Children (at the end)
|
||||
|
||||
### 2. Default Values
|
||||
|
||||
Provide sensible defaults for all optional props:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
type Status =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T };
|
||||
| { status: 'error'; error: string };
|
||||
```
|
||||
|
||||
### 4. Error Boundaries
|
||||
|
||||
Wrap all user-facing components:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
1. **Unit tests** for logic and rendering
|
||||
2. **Integration tests** for prop passing
|
||||
3. **Accessibility tests** for a11y compliance
|
||||
|
||||
Example unit test:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
1. Create the component file in `src/components/`
|
||||
2. Add TypeScript interface for props
|
||||
3. Write JSDoc with @param and @returns
|
||||
4. Add unit tests
|
||||
5. Update this document if a new pattern emerges
|
||||
6. 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 just `Icon`
|
||||
- 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
|
||||
Reference in New Issue
Block a user