@bernierllc/calendar
v1.2.2
Published
A calendar package for the tools monorepo.
Readme
/* Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */
@bernierllc/calendar
A highly customizable, accessible React calendar component for Next.js projects with support for template-driven and render prop-based customization.
Features
- 📅 Month and Week Views - Toggle between calendar views
- 🎨 Flexible Styling - Custom colors, pills, and styling logic
- 🧩 Template Tags - String-based customization with
{{fieldName}}syntax - ⚛️ Render Props - Full React component customization
- 📋 Event List - Sidebar list of events with custom rendering
- 🔍 Dynamic Filters - Custom filter UI with render props
- ♿ Accessible - WCAG 2.1 AA compliant with keyboard navigation
- 📤 ICS Export - Generate iCalendar files with recurring events
- 🎯 TypeScript - Full type safety and IntelliSense support
Installation
npm install @bernierllc/calendarQuick Start
import { Calendar } from '@bernierllc/calendar';
import type { CalendarEvent } from '@bernierllc/calendar';
const events: CalendarEvent[] = [
{
uid: '1',
summary: 'Team Meeting',
description: 'Weekly team sync',
start: '2024-01-15T10:00:00Z',
end: '2024-01-15T11:00:00Z',
categories: ['work'],
location: 'Conference Room A'
}
];
function App() {
return (
<Calendar
events={events}
categoryColors={{ work: 'bg-blue-500' }}
/>
);
}API Reference
Calendar Props
| Prop | Type | Description |
|------|------|-------------|
| events | CalendarEvent[] | Array of calendar events |
| categoryColors | Record<string, string> | Predefined color mapping for categories |
| initialView | 'week' \| 'month' | Initial calendar view (default: 'month') |
| filters | ReactNode \| Function | Custom filter UI or render prop |
| getEventColor | (event: CalendarEvent) => string | Custom color logic function |
| getEventPill | (event: CalendarEvent) => { label: string, color: string } \| null | Custom pill logic for event list |
| ellipsizeTitles | boolean | Always ellipsize event titles (default: true) |
Template-Driven Customization
| Prop | Type | Description |
|------|------|-------------|
| eventDescriptionTemplate | string | Template for event description/body |
| eventModalTemplate | string | Template for event modal content |
| eventCellTemplate | string | Template for calendar grid cells |
| eventListRowTemplate | string | Template for event list rows |
Render Prop Customization
| Prop | Type | Description |
|------|------|-------------|
| eventModalRenderer | (event: any) => ReactNode | Custom modal rendering |
| eventCellRenderer | (event: any, context: { date: Date }) => ReactNode | Custom calendar cell rendering |
| eventListRowRenderer | (event: any) => ReactNode | Custom event list row rendering |
CalendarEvent Interface
interface CalendarEvent {
uid: string; // Unique identifier
summary: string; // Event title
description?: string; // Event description
location?: string; // Event location
start: string; // ISO 8601 start date
end: string; // ISO 8601 end date
recurrenceRule?: string; // RRULE string for recurring events
status?: 'CONFIRMED' | 'TENTATIVE' | 'CANCELLED';
organizer?: string; // Organizer email/name
attendees?: string[]; // Attendee emails
categories?: string[]; // Event categories for color coding
[key: string]: any; // Custom fields supported
}Usage Examples
Basic Usage
import { Calendar } from '@bernierllc/calendar';
const events = [
{
uid: '1',
summary: 'Team Meeting',
start: '2024-01-15T10:00:00Z',
end: '2024-01-15T11:00:00Z',
categories: ['work']
}
];
<Calendar
events={events}
categoryColors={{ work: 'bg-blue-500', personal: 'bg-green-500' }}
/>Template Tags
<Calendar
events={events}
eventDescriptionTemplate="Mentor: {{mentor}}, Location: {{location}}, Type: {{experienceType}}"
eventModalTemplate="<h3>{{summary}}</h3><p>Mentor: {{mentor}}</p><p>Experience: {{experienceType}}</p>"
eventCellTemplate="{{summary}} ({{mentor}})"
eventListRowTemplate="{{summary}} - {{mentor}} - {{experienceType}}"
/>Render Props
<Calendar
events={events}
eventModalRenderer={(event) => (
<div className="custom-modal">
<h2>{event.summary}</h2>
<p>Mentor: {event.mentor}</p>
<p>Experience: {event.experienceType}</p>
<button onClick={() => handleEdit(event)}>Edit</button>
</div>
)}
eventCellRenderer={(event, { date }) => (
<div className={`event-cell ${event.experienceType}`}>
<span className="title">{event.summary}</span>
<span className="mentor">{event.mentor}</span>
</div>
)}
/>Custom Styling Logic
<Calendar
events={events}
getEventColor={(event) => {
if (event.experienceType === 'mentorship') return 'bg-purple-500';
if (event.experienceType === 'workshop') return 'bg-orange-500';
return 'bg-gray-500';
}}
getEventPill={(event) => ({
label: event.experienceType,
color: event.experienceType === 'mentorship' ? 'bg-purple-100 text-purple-800' : 'bg-orange-100 text-orange-800'
})}
/>Custom Filters
<Calendar
events={events}
filters={({ visibleEvents, filters, setFilters }) => (
<div className="filters">
<select
value={filters.experienceType || ''}
onChange={(e) => setFilters({ ...filters, experienceType: e.target.value })}
>
<option value="">All Types</option>
<option value="mentorship">Mentorship</option>
<option value="workshop">Workshop</option>
</select>
<span>{visibleEvents.length} events</span>
</div>
)}
/>ICS Export
import { generateICS } from '@bernierllc/calendar/utils/ics';
const handleExport = () => {
const icsString = generateICS(events);
const blob = new Blob([icsString], { type: 'text/calendar' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'calendar.ics';
a.click();
};Advanced Patterns
Combining Template Tags and Render Props
Template tags are used as fallbacks when render props aren't provided:
<Calendar
events={events}
eventModalTemplate="<h3>{{summary}}</h3><p>{{description}}</p>"
eventModalRenderer={(event) => {
// Custom logic for specific events
if (event.experienceType === 'mentorship') {
return <MentorshipModal event={event} />;
}
// Fallback to template for other events
return null; // Will use template
}}
/>Custom Fields with Type Safety
interface CustomEvent extends CalendarEvent {
mentor: string;
experienceType: 'mentorship' | 'workshop' | 'networking';
difficulty: 'beginner' | 'intermediate' | 'advanced';
maxParticipants?: number;
}
const events: CustomEvent[] = [
{
uid: '1',
summary: 'React Workshop',
start: '2024-01-15T10:00:00Z',
end: '2024-01-15T12:00:00Z',
mentor: 'John Doe',
experienceType: 'workshop',
difficulty: 'intermediate',
maxParticipants: 20
}
];Dynamic Filtering with Custom Fields
<Calendar
events={events}
filters={({ visibleEvents, filters, setFilters }) => (
<div className="space-y-2">
<select
value={filters.experienceType || ''}
onChange={(e) => setFilters({ ...filters, experienceType: e.target.value })}
className="border rounded px-2 py-1"
>
<option value="">All Experience Types</option>
<option value="mentorship">Mentorship</option>
<option value="workshop">Workshop</option>
<option value="networking">Networking</option>
</select>
<select
value={filters.difficulty || ''}
onChange={(e) => setFilters({ ...filters, difficulty: e.target.value })}
className="border rounded px-2 py-1"
>
<option value="">All Difficulties</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
<div className="text-sm text-gray-600">
{visibleEvents.length} events found
</div>
</div>
)}
/>Accessibility
The calendar is built with accessibility in mind:
- Keyboard Navigation - Full keyboard support for navigation and interaction
- ARIA Labels - Proper ARIA attributes for screen readers
- Focus Management - Logical tab order and focus indicators
- Semantic HTML - Proper use of table elements and landmarks
- Color Contrast - Meets WCAG 2.1 AA contrast requirements
Styling
The calendar uses Tailwind CSS classes. You can customize the appearance by:
- Overriding Tailwind classes in your CSS
- Using render props for complete control
- Using template tags for simple text customization
- Using
getEventColorandgetEventPillfor dynamic styling
Browser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
License
ISC License - see LICENSE file for details.
Contributing
See CONTRIBUTING.md for development guidelines.
Monorepo Workspaces & Dependency Management
This package is part of a monorepo using npm workspaces. All dependencies are hoisted to the root. Always run npm install from the root directory.
React 19 and Testing Library Compatibility
This package uses React 19.1.0. If you see peer dependency warnings with Testing Library, use:
npm install --legacy-peer-depsThis is a temporary workaround until official support is released.
