Component Library
Purpose
The Component Library is a comprehensive catalog of all reusable UI components available in PenguinMails. This document serves as the single source of truth for component specifications, usage patterns, and implementation details, ensuring consistent design and development practices across the platform.
π οΈ Implementation Reference
| Component Category | Codebase Location |
|---|---|
| Primitives | apps/web/components/primitives/ |
| Layout | apps/web/components/layout/ |
| Forms | apps/web/components/forms/ |
| Data Display | apps/web/components/data-display/ |
| Feedback | apps/web/components/feedback/ |
| Navigation | apps/web/components/navigation/ |
Developer Note: All components are exported from the barrel file at
apps/web/components/index.tsfor cleaner imports.
ποΈ Implementation Guidelines
Applying Design Tokens
Components should strictly use the design tokens defined in Design Tokens via Tailwind CSS utility classes.
-
Colors: Use semantic names (e.g.,
bg-primary,text-foreground). -
Spacing: Use spacing scale (e.g.,
p-4,gap-2). -
Typography: Use text utilities (e.g.,
text-sm,font-medium).
Mobile Support
Follow a mobile-first approach. Default styles should apply to mobile viewports, with md: and lg: overrides for larger screens.
-
Touch Targets: Ensure interactive elements are at least 44px high on mobile.
-
Widths: Use
w-fullon mobile for block-level actions.
Theme Support
All components must support both light and dark modes using Tailwindβs dark: modifier.
-
Backgrounds:
bg-white dark:bg-slate-950 -
Borders:
border-slate-200 dark:border-slate-800 -
Text:
text-slate-900 dark:text-slate-50
Accessibility (a11y)
-
Interactive Elements: Must be keyboard accessible (focusable).
-
ARIA Attributes: Use
aria-label,aria-expanded, etc., where visual context is missing. -
Focus States: Always define visible focus states (
focus-visible:ring-2).
Library Structure
Component Organization
π components/
βββ π primitives
βββ π layout
βββ π navigation
βββ π forms
βββ π data-display
βββ π feedback
βββ π overlays
βββ π utilities
Component Maturity Levels
-
Technical Experimental: New components under development
-
Completed Stable: Production-ready components with full test coverage
-
π Deprecated: Components scheduled for removal (with migration guides)
-
π« Removed: Components no longer available
Primitives
Button
import { Button } from '@/components/primitives/Button';
interface ButtonProps {
variant: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'success';
size: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
icon?: React.ComponentType<{ className?: string }>;
iconPosition?: 'left' | 'right';
children: React.ReactNode;
onClick: () => void;
}
// Usage Examples
<Button variant="primary" size="md" onClick={handleSubmit}>
Create Campaign
</Button>
<Button variant="secondary" icon={PlusIcon} iconPosition="left">
Add Recipient
</Button>
<Button variant="danger" size="sm" loading={isDeleting}>
Delete
</Button>
Design Specifications:
-
Height: 32px (sm), 40px (md), 48px (lg)
-
Border radius: 6px (sm, md), 8px (lg)
-
Font weight: 500 (regular), 600 (bold for primary)
-
Focus ring: 2px solid primary color, 2px offset
Input
import { Input } from '@/components/primitives/Input';
interface InputProps {
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'error' | 'success';
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
placeholder?: string;
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
onFocus?: () => void;
leftIcon?: React.ComponentType;
rightIcon?: React.ComponentType;
helperText?: string;
errorMessage?: string;
}
// Usage Example
<Input
type="email"
placeholder="Enter your email address"
value={email}
onChange={setEmail}
onBlur={() => validateEmail(email)}
errorMessage={emailError}
leftIcon={MailIcon}
required
/>
Design Specifications:
-
Height: 32px (sm), 40px (md), 48px (lg)
-
Border: 1px solid neutral-300 (default)
-
Border radius: 6px
-
Padding: 8px 12px (sm), 12px 16px (md), 16px 20px (lg)
-
Font size: 14px (sm), 16px (md), 18px (lg)
Layout Components
Container
import { Container } from '@/components/layout/Container';
interface ContainerProps {
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
centered?: boolean;
children: React.ReactNode;
}
// Usage Examples
<Container size="lg" padding="md">
<h1>Main Content</h1>
<p>Container with large max-width and medium padding</p>
</Container>
<Container size="full" centered padding="none">
<HeroSection />
</Container>
Size Specifications:
-
sm: 640px max-width
-
md: 768px max-width
-
lg: 1024px max-width
-
xl: 1280px max-width
-
full: 100% width
Grid & GridItem
import { Grid, GridItem } from '@/components/layout/Grid';
interface GridProps {
columns?: number; // 1-12
gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
responsive?: boolean;
children: React.ReactNode;
}
interface GridItemProps {
span?: number; // 1-12 columns to span
offset?: number; // 0-11 columns to offset
order?: number;
children: React.ReactNode;
}
// Usage Example
<Grid columns={12} gap="md" responsive>
<GridItem span={8}>
<MainContent />
</GridItem>
<GridItem span={4}>
<Sidebar />
</GridItem>
</Grid>
Responsive Breakpoints:
-
Mobile: 1 column (span resets to 12)
-
Tablet: 8 columns max
-
Desktop: 12 columns max
Form Components
FormField
import { FormField } from '@/components/forms/FormField';
interface FormFieldProps {
label?: string;
required?: boolean;
helperText?: string;
errorMessage?: string;
disabled?: boolean;
children: React.ReactNode;
}
// Usage Example
<FormField
label="Campaign Name"
required
helperText="Choose a descriptive name for your campaign"
errorMessage={errors.name}
>
<Input
value={campaignName}
onChange={setCampaignName}
placeholder="e.g., Summer Sale 2025"
/>
</FormField>
Select
import { Select } from '@/components/forms/Select';
interface SelectOption {
value: string | number;
label: string;
disabled?: boolean;
icon?: React.ComponentType;
}
interface SelectProps {
options: SelectOption[];
value?: string | number;
placeholder?: string;
multiple?: boolean;
searchable?: boolean;
clearable?: boolean;
loading?: boolean;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
onChange: (value: string | number | (string | number)[]) => void;
}
// Usage Example
<Select
options={[
{ value: 'draft', label: 'Draft', icon: EditIcon },
{ value: 'scheduled', label: 'Scheduled', icon: CalendarIcon },
{ value: 'sending', label: 'Sending', icon: SendIcon },
{ value: 'completed', label: 'Completed', icon: CheckIcon }
]}
value={status}
placeholder="Select campaign status"
onChange={(value) => setStatus(value as string)}
/>
Checkbox & Radio
import { Checkbox, Radio, RadioGroup } from '@/components/forms/Checkbox';
interface CheckboxProps {
checked: boolean;
disabled?: boolean;
label?: string;
helperText?: string;
onChange: (checked: boolean) => void;
}
interface RadioGroupProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
children: React.ReactNode;
}
// Usage Examples
<Checkbox
checked={agreeToTerms}
onChange={setAgreeToTerms}
label="I agree to the Terms of Service"
helperText="You must agree to continue"
/>
<RadioGroup value={planType} onChange={setPlanType}>
<Radio value="starter" label="Starter Plan" />
<Radio value="professional" label="Professional Plan" />
<Radio value="enterprise" label="Enterprise Plan" />
</RadioGroup>
Data Display Components
Table
import { Table } from '@/components/data-display/Table';
interface TableColumn<T> {
key: keyof T;
header: string;
sortable?: boolean;
filterable?: boolean;
width?: string | number;
align?: 'left' | 'center' | 'right';
render?: (value: any, row: T) => React.ReactNode;
}
interface TableProps<T> {
data: T[];
columns: TableColumn<T>[];
loading?: boolean;
selectable?: boolean;
pagination?: boolean;
pageSize?: number;
emptyState?: React.ReactNode;
onSort?: (key: keyof T, direction: 'asc' | 'desc') => void;
onSelect?: (selectedRows: T[]) => void;
onRowClick?: (row: T) => void;
}
// Usage Example
<Table
data={campaigns}
columns={[
{
key: 'name',
header: 'Campaign Name',
sortable: true,
render: (value, row) => (
<Link to={`/campaigns/${row.id}`}>{value}</Link>
)
},
{
key: 'status',
header: 'Status',
filterable: true,
render: (value) => <StatusBadge status={value} />
},
{
key: 'sent',
header: 'Sent',
sortable: true,
align: 'right'
},
{
key: 'openRate',
header: 'Open Rate',
sortable: true,
align: 'right',
render: (value) => `${(value * 100).toFixed(1)}%`
}
]}
pagination
pageSize={25}
selectable
onSelect={handleSelection}
onRowClick={handleRowClick}
/>
Card
import { Card, CardHeader, CardContent, CardFooter } from '@/components/data-display/Card';
interface CardProps {
variant?: 'default' | 'elevated' | 'outlined' | 'filled';
size?: 'sm' | 'md' | 'lg';
padding?: 'none' | 'sm' | 'md' | 'lg';
hoverable?: boolean;
clickable?: boolean;
fullWidth?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
// Usage Example
<Card variant="elevated" hoverable clickable onClick={handleCardClick}>
<CardHeader>
<Avatar src={campaign.creator.avatar} size="md" />
<div>
<h3 className="card-title">{campaign.name}</h3>
<p className="card-subtitle">{campaign.description}</p>
</div>
<Badge variant={campaign.status.variant}>
{campaign.status.label}
</Badge>
</CardHeader>
<CardContent>
<div className="metrics-grid">
<Metric label="Recipients" value={campaign.recipientCount} />
<Metric label="Open Rate" value={`${campaign.openRate}%`} />
<Metric label="Click Rate" value={`${campaign.clickRate}%`} />
</div>
</CardContent>
<CardFooter>
<Button variant="secondary" size="sm">
View Details
</Button>
<Button variant="primary" size="sm">
Edit Campaign
</Button>
</CardFooter>
</Card>
## Feedback Components
### Alert
```jsx
import { Alert } from '@/components/feedback/Alert';
interface AlertProps {
variant: 'info' | 'success' | 'warning' | 'error';
title?: string;
message: string;
dismissible?: boolean;
action?: {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
};
icon?: React.ComponentType;
onDismiss?: () => void;
}
// Usage Examples
<Alert
variant="warning"
title="Action Required"
message="Your Stripe account setup is incomplete. Complete setup to start accepting payments."
action={{
label: "Complete Setup",
onClick: () => navigate('/settings')
}}
dismissible
/>
<Alert
variant="success"
message="Campaign sent successfully! Check your analytics for performance details."
/>
Toast
import { useToast } from '@/components/feedback/Toast';
interface ToastOptions {
variant?: 'info' | 'success' | 'warning' | 'error';
title?: string;
message: string;
duration?: number;
persistent?: boolean;
action?: {
label: string;
onClick: () => void;
};
}
// Usage with Hook
const toast = useToast();
const handleSave = async () => {
try {
await saveCampaign(campaignData);
toast.success({
title: "Campaign Saved",
message: "Your campaign has been saved successfully.",
action: {
label: "View Campaign",
onClick: () => navigate(`/campaigns/${campaignData.id}`)
}
});
} catch (error) {
toast.error({
title: "Save Failed",
message: "Unable to save campaign. Please try again.",
persistent: true
});
}
};
Modal
import { Modal } from '@/components/feedback/Modal';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
description?: string;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
closable?: boolean;
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
footer?: React.ReactNode;
children: React.ReactNode;
}
// Usage Example
<Modal
isOpen={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
title="Delete Campaign"
description="This action cannot be undone. All campaign data will be permanently removed."
size="md"
footer={
<div className="modal-footer-actions">
<Button variant="secondary" onClick={() => setShowDeleteModal(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} loading={isDeleting}>
Delete Campaign
</Button>
</div>
}
>
<div className="delete-confirmation">
<p>Are you sure you want to delete <strong>"{campaignName}"</strong>?</p>
<ul className="delete-consequences">
<li>All email data will be permanently removed</li>
<li>Analytics and reports will be deleted</li>
<li>Recipients will no longer receive campaign emails</li>
</ul>
</div>
</Modal>
## Navigation Components
### Breadcrumb
```typescript
import { Breadcrumb } from '@/components/navigation/Breadcrumb';
interface BreadcrumbItem {
label: string;
href?: string;
icon?: React.ComponentType;
current?: boolean;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
separator?: React.ComponentType;
maxItems?: number;
collapsible?: boolean;
}
// Usage Example
<Breadcrumb
items={[
{ label: 'Dashboard', href: '/dashboard', icon: HomeIcon },
{ label: 'Campaigns', href: '/campaigns' },
{ label: 'Summer Sale 2025', current: true }
]}
maxItems={5}
/>
### Tabs
```typescript
import { Tabs } from '@/components/navigation/Tabs';
interface TabItem {
id: string;
label: string;
icon?: React.ComponentType;
badge?: string | number;
disabled?: boolean;
content?: React.ReactNode;
}
interface TabsProps {
tabs: TabItem[];
activeTab: string;
variant?: 'default' | 'pills' | 'underline' | 'buttons';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
lazy?: boolean; // Load tab content only when activated
onChange: (tabId: string) => void;
}
// Usage Example
<Tabs
tabs={[
{
id: 'overview',
label: 'Overview',
icon: DashboardIcon,
content: <CampaignOverview campaign={campaign} />
},
{
id: 'performance',
label: 'Performance',
badge: 'New',
content: <CampaignPerformance campaign={campaign} />
},
{
id: 'recipients',
label: 'Recipients',
content: <CampaignRecipients campaign={campaign} />
},
{
id: 'settings',
label: 'Settings',
icon: SettingsIcon,
content: <CampaignSettings campaign={campaign} />
}
]}
activeTab={activeTab}
variant="underline"
onChange={setActiveTab}
/>
## Utility Components
### Loading
```typescript
import { Loading, Skeleton } from '@/components/utilities/Loading';
interface LoadingProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
variant?: 'spinner' | 'dots' | 'pulse' | 'bars';
color?: string;
overlay?: boolean;
text?: string;
showText?: boolean;
}
interface SkeletonProps {
variant?: 'text' | 'rectangular' | 'circular';
width?: string | number;
height?: string | number;
animation?: 'pulse' | 'wave' | false;
}
// Usage Examples
<Loading size="md" variant="spinner" text="Saving changes..." />
<Skeleton variant="rectangular" width="100%" height={200} />
<Skeleton variant="text" width="60%" />
<Skeleton variant="circular" width={40} height={40} />
### EmptyState
```jsx
import { EmptyState } from '@/components/utilities/EmptyState';
interface EmptyStateProps {
icon?: React.ComponentType;
title: string;
description?: string;
size?: 'sm' | 'md' | 'lg';
action?: {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
icon?: React.ComponentType;
};
secondaryAction?: {
label: string;
onClick: () => void;
};
}
// Usage Example
<EmptyState
icon={InboxIcon}
title="No campaigns yet"
description="Create your first email campaign to get started with automated outreach."
action={{
label: "Create Campaign",
variant: "primary",
icon: PlusIcon,
onClick: () => navigate('/campaigns')
}}
secondaryAction={{
label: "Learn More",
onClick: () => openTutorial('creating-campaigns')
}}
/>
Badge
import { Badge } from '@/components/utilities/Badge';
interface BadgeProps {
variant?: 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral';
size?: 'xs' | 'sm' | 'md';
rounded?: boolean;
dot?: boolean;
icon?: React.ComponentType;
children: React.ReactNode;
}
// Usage Examples
<Badge variant="success">Active</Badge>
<Badge variant="warning" size="sm">Pending</Badge>
<Badge variant="error" dot /> {/* Status dot only */}
<Badge variant="info" icon={InfoIcon}>New Feature</Badge>
Component Status and Roadmap
Current Component Status
| Component | Status | Version | Last Updated |
|---|---|---|---|
| Button | Completed Stable | 2.1.0 | Oct 2025 |
| Input | Completed Stable | 2.0.3 | Oct 2025 |
| Select | Completed Stable | 1.8.2 | Oct 2025 |
| Table | Completed Stable | 2.2.1 | Oct 2025 |
| Modal | Completed Stable | 1.9.0 | Oct 2025 |
| Card | Completed Stable | 1.7.1 | Sep 2025 |
| Alert | Completed Stable | 1.6.0 | Sep 2025 |
| Tabs | Completed Stable | 1.5.2 | Aug 2025 |
| Tooltip | Completed Stable | 1.4.1 | Jul 2025 |
Upcoming Components
-
DatePicker: Advanced date selection with calendar
-
TimePicker: Time selection with timezone support
-
ColorPicker: Color selection for email templates
-
FileUpload: Drag-and-drop file upload with progress
-
Stepper: Multi-step process indicator
-
Accordion: Collapsible content sections
Component Deprecations
-
LegacyButton: Deprecated in favor of new Button component
-
OldModal: Deprecated, migrate to new Modal component by Dec 2025
-
BasicTable: Deprecated, use enhanced Table component
Implementation Guidelines
Import Patterns
// Preferred: Import specific components
import { Button, Input, Modal } from '@/components';
// Alternative: Import with alias for complex components
import { Table as DataTable } from '@/components/data-display/Table';
import { Chart as AnalyticsChart } from '@/components/data-display/Chart';
Component Composition
// Good: Compose components logically
<Card>
<CardHeader>
<Avatar src={user.avatar} />
<div>
<h3>{user.name}</h3>
<p>{user.role}</p>
</div>
</CardHeader>
<CardContent>
<p>{user.bio}</p>
</CardContent>
<CardFooter>
<Button variant="secondary">View Profile</Button>
<Button variant="primary">Send Message</Button>
</CardFooter>
</Card>
// Avoid: Over-composition
<div className="custom-wrapper">
<Card>
<div className="custom-header">
<CardHeader>...</CardHeader>
</div>
...
</Card>
</div>
Styling Guidelines
-
Use design tokens for all spacing, colors, and typography
-
Avoid inline styles except for dynamic values
-
Use CSS custom properties for themeable values
-
Follow BEM naming convention for custom styles
Related Documents
-
UI Library - Component usage guidelines and patterns
-
Design System - Complete design system overview
-
Design Tokens - Design token specifications
-
Accessibility Guidelines - Inclusive design standards