Appearance
Component Library
Overview
Jubiloop uses a custom component library built on top of shadcn/ui components. The library provides accessible, customizable, and reusable UI components that follow the project's design system.
Architecture
Component Organization
packages/ui/ # Shared UI package
├── src/
│ ├── components/ # UI components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── form.tsx
│ │ └── ...
│ └── lib/
│ └── utils.ts # Utility functions
apps/webapp/
├── src/
│ └── components/ # App-specific components
│ ├── auth/ # Authentication components
│ └── Header.tsx # Navigation headerComponent Layers
- Base Components (
packages/ui): Reusable UI primitives - App Components (
apps/webapp/components): Business logic components - Page Components (
apps/webapp/routes): Route-specific components
Core Components
Button
Versatile button component with multiple variants and sizes:
typescript
import { Button } from '@jubiloop/ui/components/button'
<Button variant="default" size="md">
Click me
</Button>
<Button variant="outline" size="sm">
Cancel
</Button>
<Button variant="destructive" disabled>
Delete
</Button>Variants:
default- Primary action buttonoutline- Secondary actionghost- Minimal stylingdestructive- Dangerous actionslink- Styled as a link
Form Components
Built with React Hook Form and Radix UI:
typescript
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@jubiloop/ui/components/form'
import { Input } from '@jubiloop/ui/components/input'
<Form {...form}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</Form>Card
Container component for content sections:
typescript
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@jubiloop/ui/components/card'
<Card>
<CardHeader>
<CardTitle>Event Details</CardTitle>
<CardDescription>View and manage your event</CardDescription>
</CardHeader>
<CardContent>
{/* Content */}
</CardContent>
<CardFooter>
<Button>Save Changes</Button>
</CardFooter>
</Card>Logo
Brand logo component with responsive sizing:
typescript
import { Logo } from '@jubiloop/ui/components/logo'
<Logo className="h-8 w-auto" />Toast Notifications
Using Sonner for toast notifications:
typescript
import { toast } from 'sonner'
// Success toast
toast.success('Event created successfully!')
// Error toast
toast.error('Failed to save changes')
// Promise toast
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Saved successfully!',
error: 'Failed to save',
})Business Components
Authentication Components
SignInForm
Complete sign-in form with validation:
typescript
import SignInForm from '@/components/auth/SignInForm'
<SignInForm
onSuccess={() => navigate('/dashboard')}
redirectTo="/dashboard"
/>Features:
- Email/password validation
- Remember me option
- Error handling
- Loading states
- CSRF protection
SignUpForm
User registration form:
typescript
import SignUpForm from '@/components/auth/SignUpForm'
<SignUpForm
onSuccess={() => navigate('/dashboard')}
/>Features:
- Multi-field validation
- Password strength requirements
- Terms acceptance
- Duplicate email detection
Header Component
Main navigation header with responsive design:
typescript
import Header from '@/components/Header'
<Header />Features:
- User menu with avatar
- Logout functionality
- Responsive mobile menu
- Active route highlighting
Component Patterns
Composition Pattern
Build complex UIs by composing smaller components:
typescript
function EventCard({ event }) {
return (
<Card>
<CardHeader>
<CardTitle>{event.name}</CardTitle>
<CardDescription>{event.date}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label>Location</Label>
<p>{event.location}</p>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Edit</Button>
<Button>View Details</Button>
</CardFooter>
</Card>
)
}Compound Components
Related components that work together:
typescript
<Form.Root>
<Form.Field name="email">
<Form.Label>Email</Form.Label>
<Form.Control>
<Input type="email" />
</Form.Control>
<Form.Error />
</Form.Field>
</Form.Root>Controlled Components
Form inputs with controlled state:
typescript
function ControlledInput() {
const [value, setValue] = useState('')
return (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Controlled input"
/>
)
}Styling System
Tailwind CSS Classes
Components use Tailwind CSS for styling:
typescript
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900">Title</h2>
<Button size="sm">Action</Button>
</div>CSS Variables
Theme customization through CSS variables:
css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
/* ... more variables */
}Utility Functions
Helper for conditional classes:
typescript
import { cn } from '@jubiloop/ui/lib/utils'
<div className={cn(
"base-classes",
isActive && "active-classes",
isDisabled && "disabled-classes"
)} />Accessibility
ARIA Attributes
Components include proper ARIA attributes:
typescript
<Button
aria-label="Close dialog"
aria-pressed={isPressed}
aria-disabled={isDisabled}
>
Close
</Button>Keyboard Navigation
All interactive components support keyboard navigation:
- Tab navigation
- Enter/Space activation
- Escape to close
- Arrow keys for menus
Screen Reader Support
Components include screen reader announcements:
typescript
<FormMessage role="alert" aria-live="polite">
{error.message}
</FormMessage>Component Guidelines
Creating New Components
- Start with shadcn/ui: Check if a base component exists
- Follow Naming Conventions: PascalCase for components
- Include TypeScript Types: Define prop interfaces
- Add JSDoc Comments: Document component usage
- Consider Accessibility: Include ARIA attributes
- Write Tests: Unit tests for logic, integration for behavior
Component Template
typescript
import { forwardRef } from 'react'
import { cn } from '@jubiloop/ui/lib/utils'
export interface ComponentNameProps
extends React.HTMLAttributes<HTMLDivElement> {
/** Component variant */
variant?: 'default' | 'secondary'
/** Component size */
size?: 'sm' | 'md' | 'lg'
}
/**
* ComponentName - Brief description
*
* @example
* <ComponentName variant="secondary" size="lg">
* Content
* </ComponentName>
*/
const ComponentName = forwardRef<HTMLDivElement, ComponentNameProps>(
({ className, variant = 'default', size = 'md', ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'base-styles',
variants[variant],
sizes[size],
className
)}
{...props}
/>
)
}
)
ComponentName.displayName = 'ComponentName'
export { ComponentName }Testing Components
Unit Tests
typescript
import { render, screen } from '@testing-library/react'
import { Button } from '@jubiloop/ui/components/button'
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('applies variant classes', () => {
render(<Button variant="outline">Test</Button>)
expect(screen.getByRole('button')).toHaveClass('border')
})
})Integration Tests
typescript
import { render, screen, fireEvent } from '@testing-library/react'
import SignInForm from '@/components/auth/SignInForm'
test('submits form with valid data', async () => {
const onSubmit = vi.fn()
render(<SignInForm onSubmit={onSubmit} />)
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' }
})
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' }
})
fireEvent.click(screen.getByText('Sign In'))
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
})
})
})Component Documentation
Storybook (Future)
Component documentation and visual testing:
typescript
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '@jubiloop/ui/components/button'
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['default', 'outline', 'ghost', 'destructive', 'link'],
},
},
}
export default meta
export const Default: StoryObj<typeof Button> = {
args: {
children: 'Button',
variant: 'default',
},
}Performance Optimization
Code Splitting
Lazy load heavy components:
typescript
const HeavyComponent = lazy(() => import('./HeavyComponent'))
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>Memoization
Prevent unnecessary re-renders:
typescript
const MemoizedComponent = memo(({ data }) => {
return <ExpensiveComponent data={data} />
}, (prevProps, nextProps) => {
return prevProps.data.id === nextProps.data.id
})Virtual Scrolling
For long lists:
typescript
import { VirtualList } from '@tanstack/react-virtual'
<VirtualList
height={600}
itemCount={items.length}
itemSize={50}
renderItem={({ index }) => (
<ListItem item={items[index]} />
)}
/>