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