swimline-calendar
v0.3.2
Published
A React calendar component with swimlane layout
Downloads
58
Readme
Swimline Calendar
A React calendar component with swimlane layout for appointment and resource management.
Installation
npm install swimline-calendarRequires React 19 or later (declared as a peer dependency).
Basic Usage
import { SwimlineCalendar } from 'swimline-calendar';
import 'swimline-calendar/styles.css';
function App() {
const resources = [
{
id: 'r1',
title: 'John Doe',
subtitle: 'Technician',
appointments: [
{
id: 'apt1',
title: 'Client Meeting',
startDate: new Date(2024, 0, 15, 9, 0),
endDate: new Date(2024, 0, 15, 11, 0),
note: 'Client: Acme Corp',
color: '#3b82f6'
}
]
}
];
return (
<SwimlineCalendar
resources={resources}
/>
);
}Props
SwimlineCalendar
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| resources | ResourceType[] | ✅ | - | List of resources with their appointments |
| startHour | number | ❌ | 8 | Start hour of the day view time range (0-23) |
| endHour | number | ❌ | 18 | End hour of the day view time range (0-24) |
| theme | 'light' \| 'dark' | ❌ | 'light' | Calendar theme |
| tooltipPosition | 'top' \| 'bottom' \| 'left' \| 'right' | ❌ | 'bottom' | Position of the appointment details tooltip |
| onAppointmentMove | (appointmentId, resourceId, newStartDate, newEndDate) => void | ❌ | - | Callback fired when an appointment is moved |
| minGap | number | ❌ | - | Minimum time gap between appointments (in minutes) |
| backlog | BacklogAppointment[] | ❌ | - | List of unscheduled appointments |
| onAppointmentSchedule | (backlogId, resourceId, startDate) => void | ❌ | - | Callback fired when scheduling an appointment from the backlog |
| dropZones | DropZone[] | ❌ | - | Drop zones rendered below the calendar for custom drag-and-drop actions |
| locale | 'fr' \| 'en' | ❌ | 'en' | Interface language |
| translations | Partial<Translations> | ❌ | - | Custom translations |
Data Types
ResourceType
interface ResourceType {
id: string;
title: string;
subtitle?: string;
appointments: AppointmentType[];
}AppointmentType
interface AppointmentType {
id: string;
title: string;
startDate: Date;
endDate: Date;
note?: string;
color?: string;
}BacklogAppointment
interface BacklogAppointment {
id: string;
title: string;
duration: number; // Duration in hours (e.g., 1.5 for 1h30)
note?: string;
color?: string;
resourceId?: string;
}DropZone
interface DropZone {
label: string; // Text displayed inside the zone
onDrop: (payload: DropZonePayload) => void; // Fired when an appointment is dropped
}DropZonePayload
interface DropZonePayload {
appointmentId: string;
resourceId?: string; // Source resource, when applicable
source: 'calendar' | 'backlog'; // Where the dragged item came from
appointment?: AppointmentType; // Resolved appointment (source: 'calendar')
backlogAppointment?: BacklogAppointment; // Resolved item (source: 'backlog')
}Features
📅 Multiple views
The calendar supports three view modes:
- Day — detailed view of a single day
- Week — full week view
- Month — monthly view
🖱️ Drag & Drop
Appointments can be moved by drag-and-drop:
- Between resources
- To different time slots
- The component automatically snaps to the nearest quarter-hour
const handleAppointmentMove = (
appointmentId: string,
newResourceId: string,
newStartDate: Date,
newEndDate: Date
) => {
// Update your data
console.log(`Appointment ${appointmentId} moved to ${newResourceId}`);
};
<SwimlineCalendar
resources={resources}
onAppointmentMove={handleAppointmentMove}
/>🕐 Collision detection (Buffer)
Enable collision detection to prevent overlapping appointments:
<SwimlineCalendar
resources={resources}
minGap={15} // 15-minute gap between appointments
/>A red visual indicator appears on an invalid drop attempt.
📋 Backlog
Manage unscheduled appointments with the backlog system:
const [backlog, setBacklog] = useState([
{
id: 'b1',
title: 'HVAC Installation',
duration: 2, // 2 hours
note: 'Client: Mr. Dubois',
color: '#0d9488'
}
]);
const handleAppointmentSchedule = (backlogId, resourceId, startDate) => {
// Move the backlog item onto the calendar
const item = backlog.find(b => b.id === backlogId);
// Compute endDate from the duration
const endDate = new Date(startDate);
endDate.setHours(endDate.getHours() + item.duration);
// Add it to the resource, then remove it from the backlog
};
<SwimlineCalendar
resources={resources}
backlog={backlog}
onAppointmentSchedule={handleAppointmentSchedule}
/>🎯 Drop zones
Render custom drop zones below the calendar. Drag an appointment onto a zone to trigger an action. The library stays agnostic of your business logic — it only reports the drop event together with the dropped item's data.
import { SwimlineCalendar } from 'swimline-calendar';
import type { DropZone, DropZonePayload } from 'swimline-calendar';
const dropZones: DropZone[] = [
{
label: 'Delete appointment',
onDrop: ({ appointmentId }: DropZonePayload) => {
// Remove this appointment from your data
},
},
{
label: 'Delete recurrence',
onDrop: ({ appointment }: DropZonePayload) => {
// Remove every appointment of this recurrence
},
},
{
label: 'Send back to backlog',
onDrop: ({ appointment }: DropZonePayload) => {
// Move the appointment back to your backlog
},
},
];
<SwimlineCalendar
resources={resources}
dropZones={dropZones}
/>Each zone receives a DropZonePayload. The library resolves the full
appointment (or backlogAppointment) from your data, so all the business
logic — deletion, recurrence handling, moving back to the backlog — stays in
your project. Drop zones work across all three views and also accept items
dragged from the backlog.
🌍 Internationalization (i18n)
The calendar supports multiple languages:
Built-in languages:
// English (default)
<SwimlineCalendar locale="en" resources={resources} />
// French
<SwimlineCalendar locale="fr" resources={resources} />Custom translations (any language):
// Italian
<SwimlineCalendar
resources={resources}
translations={{
day: 'Giorno',
week: 'Settimana',
month: 'Mese',
today: 'Oggi',
toSchedule: 'Da pianificare',
noAppointments: 'Nessun appuntamento da pianificare'
}}
/>
// Spanish
<SwimlineCalendar
resources={resources}
translations={{
day: 'Día',
week: 'Semana',
month: 'Mes',
today: 'Hoy',
toSchedule: 'Por planificar',
noAppointments: 'No hay citas por planificar'
}}
/>
// Partial override (English base + customization)
<SwimlineCalendar
locale="en"
resources={resources}
translations={{
today: 'Right now' // Override a single label
}}
/>Date formatting automatically adapts to the selected locale.
🎨 Customization
The calendar uses CSS variables for theming:
:root {
--swimline-accent: #0d9488;
--swimline-bg-primary: #ffffff;
--swimline-bg-secondary: #f3f4f6;
--swimline-text-primary: #111827;
--swimline-text-secondary: #6b7280;
--swimline-border-primary: #e5e7eb;
}Appointment colors (the color field) accept any valid CSS color, in any format
or case — #3b82f6, #3B82F6, teal, rgb(59, 130, 246). You are not limited
to a fixed palette.
Full example
import { useState } from 'react';
import { SwimlineCalendar } from 'swimline-calendar';
import 'swimline-calendar/styles.css';
function App() {
const [resources, setResources] = useState([
{
id: 'tech1',
title: 'Sarah Johnson',
subtitle: 'Senior Technician',
appointments: [
{
id: 'apt1',
title: 'HVAC Installation',
startDate: new Date(2024, 0, 15, 9, 0),
endDate: new Date(2024, 0, 15, 11, 0),
note: 'Client: Downtown Restaurant',
color: '#0d9488'
}
]
}
]);
const handleAppointmentMove = (appointmentId, newResourceId, newStartDate, newEndDate) => {
setResources(prevResources => {
return prevResources.map(resource => {
// Remove the appointment from every resource
const appointments = resource.appointments.filter(apt => apt.id !== appointmentId);
// Add it to the new resource
if (resource.id === newResourceId) {
const movedApt = prevResources
.flatMap(r => r.appointments)
.find(apt => apt.id === appointmentId);
if (movedApt) {
appointments.push({
...movedApt,
startDate: newStartDate,
endDate: newEndDate,
});
}
}
return { ...resource, appointments };
});
});
};
const dropZones = [
{
label: 'Delete appointment',
onDrop: ({ appointmentId }) => {
setResources(prevResources =>
prevResources.map(resource => ({
...resource,
appointments: resource.appointments.filter(apt => apt.id !== appointmentId),
}))
);
},
},
];
return (
<SwimlineCalendar
resources={resources}
startHour={8}
endHour={18}
theme="light"
tooltipPosition="top"
onAppointmentMove={handleAppointmentMove}
minGap={15}
dropZones={dropZones}
/>
);
}License
MIT © Anna-Angélique PORTE
