Back to catalog

React Component Patterns

Guides development of React components using BrokeForge's established patterns for CardList, CardTable, CardContainer, status badges, metadata display, icons, menus, and layout conventions.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: React Component Patterns
description: Guides development of React components using BrokeForge's established patterns for CardList, CardTable, CardContainer, status badges, metadata display, icons, menus, and layout conventions.
allowed-tools: Read, Write, Edit, Glob, Grep, mcp__laravel-boost__*
---

# React Component Patterns

## Context
You are building React components for BrokeForge, following established patterns for consistent UI/UX. This skill ensures components match existing conventions for layout, metadata display, status indicators, actions, and visual hierarchy.

## Core Components

### CardContainer
The foundational container component for sections with consistent styling.

**Usage:**
```tsx
import { CardContainer } from '@/components/ui/card-container';

<CardContainer
  title="Section Title"
  description="Optional description text"
  icon={<Icon />} // Optional icon (usually commented out in current implementation)
  action={<CardContainerAddButton label="Add Item" onClick={handleAdd} />}
  parentBorder={true} // Default: true. Set false when children have their own borders
>
  {/* Content here */}
</CardContainer>
```

**Key Props:**
- `title`: Section heading (required)
- `description`: Subtitle/description (optional)
- `icon`: ReactNode icon (optional, currently commented out in implementation)
- `action`: Action button/element displayed on the right (optional)
- `parentBorder`: Boolean - wraps children in border/padding if true (default: true)
- `className`: Additional CSS classes

**When to use parentBorder={false}:**
- When children have their own card/border styling (e.g., CardList, CardTable)
- When rendering multiple bordered items inside

### CardList
Reusable list component with consistent item rendering, actions dropdown, and empty states.

**Usage:**
```tsx
import { CardList, type CardListAction } from '@/components/card-list';

<CardList<DatabaseItem>
  title="Databases"
  description="Manage your database services"
  icon={<DatabaseIcon />}
  onAddClick={() => setShowModal(true)}
  addButtonLabel="Add Database" // Optional, icon-only if omitted
  items={databases}
  keyExtractor={(db) => db.id}
  renderItem={(db) => (
    <div className="flex items-center justify-between gap-3">
      {/* Left: Primary info */}
      <div className="min-w-0 flex-1">
        <div className="truncate text-sm font-medium">
          {db.name} {db.version}
        </div>
        <div className="truncate text-xs text-muted-foreground">
          Port {db.port} · {db.metadata}
        </div>
      </div>

      {/* Right: Status badge */}
      <div className="flex-shrink-0">
        <StatusBadge status={db.status} />
      </div>
    </div>
  )}
  actions={(db) => [
    {
      label: 'Edit',
      onClick: () => handleEdit(db),
      icon: <Pencil className="h-4 w-4" />,
      disabled: db.status === 'pending'
    },
    {
      label: 'Delete',
      onClick: () => handleDelete(db),
      variant: 'destructive',
      icon: <Trash2 className="h-4 w-4" />
    }
  ]}
  emptyStateMessage="No items yet"
  emptyStateIcon={<DatabaseIcon className="h-6 w-6 text-muted-foreground" />}
/>
```

**Key Props:**
- `title`: Section title (required)
- `description`: Section description (optional)
- `icon`: Icon for the section header (optional)
- `onAddClick`: Handler for add button - shows + button if provided (optional)
- `addButtonLabel`: Label for add button - icon-only if not provided (optional)
- `items`: Array of items to display (required)
- `renderItem`: Function to render each item (required)
- `keyExtractor`: Function to extract unique key from item (defaults to index)
- `onItemClick`: Makes items clickable (optional)
- `actions`: Array or function returning CardListAction[] for dropdown menu (optional)
- `emptyStateMessage`: Custom empty state text (optional)
- `emptyStateIcon`: Custom empty state icon (optional)
- `className`: Additional CSS classes (optional)

**CardListAction Interface:**
```tsx
interface CardListAction {
  label: string;              // Action label in dropdown
  onClick: (item: any) => void; // Handler receives the item
  variant?: 'default' | 'destructive'; // 'destructive' for delete/remove
  icon?: ReactNode;           // Optional icon before label
  disabled?: boolean;         // Whether action is disabled
}
```

**Layout Pattern for renderItem:**
```tsx
// Standard layout: Left content (flex-1), Right status badge (flex-shrink-0)
// IMPORTANT: Only ONE status badge should be shown at a time based on item.status
// The status badge MUST always be positioned on the right-hand side BEFORE the ellipsis menu
<div className="flex items-center justify-between gap-3">
  {/* Left: Primary info (expands to fill) */}
  <div className="min-w-0 flex-1">
    <div className="truncate text-sm font-medium">
      {/* Primary text */}
    </div>
    <div className="truncate text-xs text-muted-foreground">
      {/* Secondary metadata */}
    </div>
  </div>

  {/* Right: Status badge (fixed width) - positioned before the ellipsis menu */}
  {/* Only ONE status badge at a time - use conditional logic based on item.status */}
  <div className="flex-shrink-0">
    <StatusBadge status={item.status} />
  </div>
  {/* The ellipsis (three-dot) menu is automatically rendered by CardList after the status badge */}
</div>
```

### CardTable
Table component for tabular data with flexbox column layout.

**Usage:**
```tsx
import { CardTable, type CardTableColumn } from '@/components/ui/card-table';

const columns: CardTableColumn<Rule>[] = [
  {
    header: 'Name',
    accessor: (row) => row.name,
    align: 'left'
  },
  {
    header: 'Port',
    accessor: (row) => row.port,
    align: 'center'
  },
  {
    header: 'Status',
    cell: (row) => <Badge>{row.status}</Badge>,
    align: 'right'
  }
];

<CardContainer title="Firewall Rules">
  <CardTable
    columns={columns}
    data={rules}
    emptyState={<EmptyState />}
    getRowKey={(row) => row.id}
    rowClassName={(row) => row.isHighlighted ? 'bg-blue-50' : ''}
    pagination={{
      currentPage: 1,
      totalPages: 10,
      totalItems: 100,
      perPage: 10,
      onPageChange: (page) => handlePageChange(page)
    }}
  />
</CardContainer>
```

**Key Props:**
- `columns`: Array of column configurations (required)
- `data`: Table data array (required)
- `emptyState`: React node for empty state (optional)
- `rowClassName`: Function to compute row classes based on row data (optional)
- `getRowKey`: Function to extract unique key (defaults to index)
- `pagination`: Pagination configuration (optional)

**CardTableColumn Interface:**
```tsx
interface CardTableColumn<T> {
  header: string;                      // Column header text
  accessor?: (row: T) => ReactNode;    // Simple accessor function
  cell?: (row: T, index: number) => ReactNode; // Custom cell renderer
  align?: 'left' | 'center' | 'right'; // Text alignment
  headerClassName?: string;            // Custom header classes
  cellClassName?: string;              // Custom cell classes
}
```

## Status Badges

### CardBadge Component (Recommended)
Use the `CardBadge` component for consistent status indicators across the application:

```tsx
import { CardBadge } from '@/components/ui/card-badge';

// Simple usage with predefined variants
<CardBadge variant="active" />
<CardBadge variant="installing" />
<CardBadge variant="failed" />

// Custom label (overrides default)
<CardBadge variant="active" label="Running" />

// Custom icon (overrides default)
<CardBadge variant="active" icon={<CustomIcon />} />
```

**Available Variants:**
- `pending` - Gray with loading spinner
- `installing` - Blue with loading spinner
- `updating` - Blue with loading spinner
- `active` - Green with checkmark (displays "Installed" by default)
- `inactive` - Amber with pause icon
- `failed` - Red with X icon
- `removing` - Blue with loading spinner
- `uninstalling` - Blue with loading spinner
- `stopped` - Gray (no icon)

**Component Features:**
- Consistent styling across all status badges
- Built-in dark mode support
- Predefined icons and labels for each variant
- Customizable labels and icons when needed
- Bordered design with proper color schemes

### Standard Status Badge Pattern (Manual)
For custom status badges not covered by CardBadge variants, use inline badge spans with consistent color scheme:

```tsx
{/* Pending */}
{item.status === 'pending' && (
  <span className="inline-flex items-center gap-1 rounded bg-slate-500/10 px-1.5 py-0.5 text-xs text-slate-600 dark:text-slate-400">
    <Loader2 className="h-3 w-3 animate-spin" />
    Pending
  </span>
)}

{/* Installing/Processing */}
{item.status === 'installing' && (
  <span className="inline-flex items-center gap-1 rounded bg-blue-500/10 px-1.5 py-0.5 text-xs text-blue-600 dark:text-blue-400">
    <Loader2 className="h-3 w-3 animate-spin" />
    Installing
  </span>
)}

{/* Active/Success */}
{item.status === 'active' && (
  <span className="inline-flex items-center gap-1 rounded bg-emerald-500/10 px-1.5 py-0.5 text-xs text-emerald-600 dark:text-emerald-400">
    <CheckCircle className="h-3 w-3" />
    Active
  </span>
)}

{/* Failed/Error */}
{item.status === 'failed' && (
  <span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-xs text-red-600 dark:text-red-400">
    <AlertCircle className="h-3 w-3" />
    Failed
  </span>
)}

{/* Updating */}
{item.status === 'updating' && (
  <span className="inline-flex items-center gap-1 rounded bg-blue-500/10 px-1.5 py-0.5 text-xs text-blue-600 dark:text-blue-400">
    <Loader2 className="h-3 w-3 animate-spin" />
    Updating
  </span>
)}

{/* Uninstalling/Removing */}
{item.status === 'uninstalling' && (
  <span className="inline-flex items-center gap-1 rounded bg-orange-500/10 px-1.5 py-0.5 text-xs text-orange-600 dark:text-orange-400">
    <Loader2 className="h-3 w-3 animate-spin" />
    Uninstalling
  </span>
)}

{/* Stopped/Inactive */}
{item.status === 'stopped' && (
  <span className="inline-flex items-center gap-1 rounded bg-gray-500/10 px-1.5 py-0.5 text-xs text-gray-600 dark:text-gray-400">
    Stopped
  </span>
)}

{/* Warning/Inactive */}
{item.status === 'inactive' && (
  <span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-xs text-amber-600 dark:text-amber-400">
    <Pause className="h-3 w-3" />
    Inactive
  </span>
)}
```

### Status Color Scheme
- **Pending**: `slate-500/10` background, `slate-600` text (dark: `slate-400`)
- **Installing/Processing/Updating**: `blue-500/10` background, `blue-600` text (dark: `blue-400`)
- **Active/Success**: `emerald-500/10` background, `emerald-600` text (dark: `emerald-400`)
- **Failed/Error**: `red-500/10` background, `red-600` text (dark: `red-400`)
- **Uninstalling/Removing**: `orange-500/10` background, `orange-600` text (dark: `orange-400`)
- **Stopped**: `gray-500/10` background, `gray-600` text (dark: `gray-400`)
- **Inactive/Warning**: `amber-500/10` background, `amber-600` text (dark: `amber-400`)

### Status Badge Classes Pattern
```tsx
className="inline-flex items-center gap-1 rounded bg-{color}-500/10 px-1.5 py-0.5 text-xs text-{color}-600 dark:text-{color}-400"
```

### Status Icons
- **Loading states**: `<Loader2 className="h-3 w-3 animate-spin" />`
- **Success**: `<CheckCircle className="h-3 w-3" />`
- **Error**: `<AlertCircle className="h-3 w-3" />`
- **Warning**: `<AlertCircle className="h-3 w-3" />`
- **Inactive**: `<Pause className="h-3 w-3" />`

### Badge Component (UI Library)
For simpler use cases, use the Badge component from `@/components/ui/badge`:

```tsx
import { Badge } from '@/components/ui/badge';

<Badge variant="default">Active</Badge>
<Badge variant="secondary">Pending</Badge>
<Badge variant="destructive">Failed</Badge>
<Badge variant="outline">Custom</Badge>

// Custom colors
<Badge className="border-green-200 bg-green-100 text-green-800">
  Allow
</Badge>
```

## Site Avatar

### SiteAvatar Component
Use the `SiteAvatar` component for consistent site icons/avatars across the application:

```tsx
import { SiteAvatar } from '@/components/site-avatar';

// Default size (medium)
<SiteAvatar domain="example.com" />

// Small size
<SiteAvatar domain="example.com" size="sm" />

// Large size
<SiteAvatar domain="example.com" size="lg" />
```

**Available Sizes:**
- `sm` - 32px (size-8, text-sm)
- `md` - 40px (size-10, text-lg) - Default
- `lg` - 48px (size-12, text-xl)

**Component Features:**
- Automatically generates color based on domain (consistent across app)
- Displays first letter of domain as initial
- 7 predefined colors (blue, purple, pink, green, yellow, red, indigo)
- Rounded square avatar with white text
- Same domain always gets same color

## Metadata Display Patterns

### Primary + Secondary Info
Always structure item info as primary (medium weight) + secondary (muted):

```tsx
<div className="min-w-0 flex-1">
  {/* Primary: Name, title, or main identifier */}
  <div className="truncate text-sm font-medium">
    {item.name} {item.version}
  </div>

  {/* Secondary: Metadata with middle dot separator */}
  <div className="truncate text-xs text-muted-foreground">
    Port {item.port} · {item.detail}
  </div>
</div>
```

### Multi-line Metadata
For more complex metadata, use multiple lines:

```tsx
<div className="min-w-0 flex-1">
  {/* Primary */}
  <div className="flex items-center gap-2">
    <h4 className="truncate text-sm font-medium">{task.name}</h4>
    <StatusBadge status={task.status} />
  </div>

  {/* Secondary: Command */}
  <p className="mt-1 truncate font-mono text-xs text-muted-foreground">
    {task.command}
  </p>

  {/* Tertiary: Additional metadata with dots */}
  <div className="mt-1.5 flex items-center gap-3 text-xs text-muted-foreground">
    <span>{task.working_directory}</span>
    <span>•</span>
    <span>{task.processes} {task.processes === 1 ? 'process' : 'processes'}</span>
    <span>•</span>
    <span>User: {task.user}</span>
  </div>
</div>
```

### Text Styling Conventions
- **Primary text**: `text-sm font-medium` or `text-sm font-medium text-foreground`
- **Secondary text**: `text-xs text-muted-foreground`
- **Commands/code**: Add `font-mono` - `font-mono text-xs text-muted-foreground`
- **Truncation**: Use `truncate` class for text that might overflow
- **Separators**: Use middle dot `·` or bullet `•` between metadata items

## Icons

### Lucide React Icons
Import icons from `lucide-react`:

```tsx
import {
  AlertCircle,
  CheckCircle,
  Clock,
  Database,
  Eye,
  Loader2,
  MoreVertical,
  Pause,
  Pencil,
  Play,
  RefreshCw,
  RotateCw,
  Shield,
  Trash2
} from 'lucide-react';
```

### Icon Sizing
- **Small icons** (badges, inline): `h-3 w-3` or `h-4 w-4`
- **Medium icons** (buttons, decorative): `h-5 w-5`
- **Large icons** (empty states, headers): `h-6 w-6` or `h-12 w-12`
- **Loading spinners**: Always use `Loader2` with `animate-spin`

### Custom SVG Icons
For custom icons, follow this pattern:

```tsx
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
  <circle cx="6" cy="6" r="5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
  <path d="M6 3v3l2 1" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
</svg>
```

## Actions & Menus

### Dropdown Menu (Three-dot Menu)
CardList automatically includes dropdown menu with MoreVertical icon. Actions are defined in the `actions` prop:

```tsx
actions={(item) => {
  const actions: CardListAction[] = [];
  const isInTransition = item.status === 'pending' || item.status === 'installing';

  // Conditional actions based on status
  if (item.status === 'active') {
    actions.push({
      label: 'Edit',
      onClick: () => handleEdit(item),
      icon: <Pencil className="h-4 w-4" />,
      disabled: isInTransition
    });
  }

  if (item.status === 'failed') {
    actions.push({
      label: 'Retry',
      onClick: () => handleRetry(item),
      icon: <RotateCw className="h-4 w-4" />,
      disabled: processing
    });
  }

  // Destructive action always last
  actions.push({
    label: 'Delete',
    onClick: () => handleDelete(item),
    variant: 'destructive',
    icon: <Trash2 className="h-4 w-4" />,
    disabled: isInTransition
  });

  return actions;
}}
```

### Manual Dropdown Menu
For custom implementations outside CardList:

```tsx
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { MoreVertical } from 'lucide-react';

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
      <MoreVertical className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuItem onClick={() => handleEdit(item)}>
      <Pencil className="mr-2 h-4 w-4" />
      Edit
    </DropdownMenuItem>
    <DropdownMenuItem
      onClick={() => handleDelete(item)}
      className="text-red-600"
    >
      <Trash2 className="mr-2 h-4 w-4" />
      Delete
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

## Empty States

### CardList Empty State
CardList handles empty states automatically:

```tsx
<CardList
  items={items}
  emptyStateMessage="No databases installed on this server yet."
  emptyStateIcon={<DatabaseIcon className="h-6 w-6 text-muted-foreground" />}
  // ... other props
/>
```

### Custom Empty State Component
For manual empty states:

```tsx
<div className="flex flex-col items-center justify-center px-6 py-8 text-center">
  <div className="mb-3 rounded-full bg-muted p-3">
    <DatabaseIcon className="h-6 w-6 text-muted-foreground" />
  </div>
  <p className="text-sm text-muted-foreground">No items yet</p>
  <p className="mt-1 text-xs text-muted-foreground">
    Additional descriptive text here
  </p>
</div>
```

### InstallSkeleton Pattern
For features requiring installation:

```tsx
import { InstallSkeleton } from '@/components/install-skeleton';

<CardContainer title="Supervisor">
  <InstallSkeleton
    icon={Eye}
    title="Supervisor Not Installed"
    description="Install Supervisor to manage long-running processes."
    buttonLabel="Install Supervisor"
    onInstall={handleInstall}
    isInstalling={processing}
  />
</CardContainer>
```

## Tabs & Toggle Groups

### Toggle Tab Pattern
For switching between views or options, use this inline tab pattern:

```tsx
import { useState } from 'react';
import { cn } from '@/lib/utils';

const [activeTab, setActiveTab] = useState<'metrics' | 'monitors'>('metrics');

const tabs = [
  { value: 'metrics' as const, label: 'Metrics' },
  { value: 'monitors' as const, label: 'Monitors' },
];

<div className="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800">
  {tabs.map(({ value, label }) => (
    <button
      key={value}
      onClick={() => setActiveTab(value)}
      className={cn(
        'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
        activeTab === value
          ? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
          : 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60'
      )}
    >
      <span className="text-sm">{label}</span>
    </button>
  ))}
</div>

{/* Conditional rendering based on active tab */}
{activeTab === 'metrics' && <MetricsView />}
{activeTab === 'monitors' && <MonitorsView />}
```

### Toggle Tab with Icons
```tsx
import { Activity, Bell } from 'lucide-react';

const tabs = [
  { value: 'metrics', icon: Activity, label: 'Metrics' },
  { value: 'monitors', icon: Bell, label: 'Monitors' },
];

<div className="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800">
  {tabs.map(({ value, icon: Icon, label }) => (
    <button
      key={value}
      onClick={() => setActiveTab(value)}
      className={cn(
        'flex items-center rounded-md px-3.5 py