Files
FrenoCorp/agents/hermes/docs/COMPONENT_PATTERNS.md
2026-03-09 13:16:37 -04:00

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:

  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:

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:

  1. Unit tests for logic and rendering
  2. Integration tests for prop passing
  3. 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:

  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