@pangeasi/lane-scheduler-react
v1.2.1
Published
Flexible drag-and-drop scheduler component for React
Downloads
335
Maintainers
Readme
Lane Scheduler
A flexible, drag-and-drop scheduler component for React with full TypeScript support.
Unit Agnostic: This library doesn't assume any specific unit of measurement. Slots represent abstract spaces that can be mapped to any unit - time intervals, physical spaces, resource allocations, or any other sequential organization system.
� Interactive Documentation
Explore the complete Storybook documentation with interactive examples, API reference, and usage guides!
Features
- 🎯 Drag & Drop - Move appointments between lanes with smooth animations
- 📏 Resizable - Adjust appointment duration from both ends
- 🔒 Locked Slots - Block specific slots with custom logic
- 🎨 Customizable - Full control over rendering with render props
- 📱 Mobile Ready - Touch events support
- ⚡ TypeScript - Full type safety
- 🪶 Lightweight - No dependencies except React
Installation
npm install @pangeasi/lane-scheduler-reactStyles
The library comes with pre-built CSS that includes all necessary Tailwind styles. You need to import it in your application:
import "@pangeasi/lane-scheduler-react/styles.css";The CSS file is self-contained and includes only the Tailwind utilities used by the components (~15KB gzipped).
Basic Usage
import { Scheduler, Lane } from "@pangeasi/lane-scheduler-react";
import "@pangeasi/lane-scheduler-react/styles.css";
function App() {
const [appointments, setAppointments] = useState([
{ id: "1", startSlot: 2, duration: 3, title: "Meeting" },
]);
const handleMove = (apt, sourceLane, targetLane, newSlot) => {
// Handle appointment move
};
return (
<Scheduler onAppointmentMove={handleMove}>
<Lane
laneId="room-1"
appointments={appointments}
totalSlots={24} // Could represent hours, rooms, days, etc.
config={{
height: 80,
slotWidth: 60,
}}
/>
</Scheduler>
);
}Props
Scheduler
| Prop | Type | Description |
| ------------------- | ----------- | ------------------------------- |
| children | ReactNode | Lane components |
| onAppointmentMove | function | Callback when appointment moves |
| collisionStrategy | "reject" \| "swap" | Defaults to "reject". Use "swap" to exchange appointments when a dragged appointment is dropped over a non-overlappable appointment |
onAppointmentMove receives a fifth details argument when you need to handle swaps:
type AppointmentMoveDetails = {
operation: "move" | "swap";
appointment: Appointment;
sourceLaneId: string;
targetLaneId: string;
newStartSlot: number;
swappedAppointment?: Appointment;
swappedAppointmentNewLaneId?: string;
swappedAppointmentNewStartSlot?: number;
};Lane
| Prop | Type | Default | Description |
| --------------------------------- | --------------- | -------- | ------------------------------------------------- |
| laneId | string | required | Unique identifier |
| appointments | Appointment[] | [] | Array of appointments |
| blockedSlots | number[] | [] | Blocked slot indices |
| totalSlots | number | 24 | Total number of slots (unit agnostic) |
| config | LaneConfig | {} | Visual configuration |
| renderSlot | function | - | Custom slot renderer |
| renderAppointmentContent | function | - | Custom appointment content |
| onSlotClick | function | - | Slot click handler |
| onSlotDoubleClick | function | - | Slot double-click handler |
| onAppointmentChange | function | - | Appointment change handler |
| onContextMenu | function | - | Slot right-click/long-press handler (default menu suppressed) |
| appointmentContainerClassName | string | - | Custom className for appointment container |
| appointmentResizerStartClassName | string | - | Custom className for start-edge resizer outer |
| appointmentResizerEndClassName | string | - | Custom className for end-edge resizer outer |
| appointmentResizerStartInnerClassName | string | - | Custom className for start-edge resizer inner line |
| appointmentResizerEndInnerClassName | string | - | Custom className for end-edge resizer inner line |
Appointment Properties
| Property | Type | Description |
| -------------- | --------- | -------------------------------------------------------------------------------------------------------- |
| id | string | Unique identifier |
| startSlot | number | Starting slot index |
| duration | number | Number of slots the appointment spans |
| title | string | Display text |
| locked | boolean | If true, prevents dragging and resizing |
| allowOverlap | boolean | If true, OTHER appointments are allowed to overlap with this one. Does NOT mean this can overlap others |
| onBlockedSlot| function| Custom logic to handle blocked slots (return true to allow placement) |
Advanced Usage
Custom Slot Rendering
// Example: Time-based slots (30-minute intervals)
<Lane
renderSlot={(slotIdx, isBlocked) => (
<div>{slotIdx % 2 === 0 ? `${slotIdx / 2}:00` : ":30"}</div>
)}
/>
// Example: Room-based slots
<Lane
renderSlot={(slotIdx, isBlocked) => (
<div>Room {slotIdx + 1}</div>
)}
/>
// Example: Day-based slots
<Lane
renderSlot={(slotIdx, isBlocked) => (
<div>Day {slotIdx + 1}</div>
)}
/>Note: The library doesn't impose any unit interpretation. You define what each slot represents through your custom rendering logic.
Slot Interactions
Use the slot event handlers to trigger creation flows, detail drawers, or custom context menus. The native context menu is suppressed when onContextMenu is provided.
<Lane
onSlotClick={(slotIdx, laneId) => openDetails(slotIdx, laneId)}
onSlotDoubleClick={(slotIdx, laneId) => createAppointment(slotIdx, laneId)}
onContextMenu={(slotIdx, laneId) => showContextMenu(slotIdx, laneId)}
/>Blocking Overlaps with allowOverlap
The allowOverlap property controls whether OTHER appointments can overlap with this appointment:
// Appointments with allowOverlap: true allow others to be placed over them
const flexibleAppointment = {
id: "1",
startSlot: 5,
duration: 2,
title: "Flexible Meeting",
allowOverlap: true, // Other appointments can overlap with this one
};
// Appointments without allowOverlap (default: false) block overlapping
const strictAppointment = {
id: "2",
startSlot: 6,
duration: 2,
title: "Exclusive Meeting",
allowOverlap: false, // No other appointments can overlap with this one
};
// When dragging/resizing appointments:
// - They can only overlap with appointments that have allowOverlap: true
// - If they try to overlap with an appointment where allowOverlap: false,
// the drop/resize is rejected and shown in redSwapping Appointments
Set collisionStrategy="swap" on Scheduler to exchange two appointments instead of rejecting a drop over a non-overlappable appointment.
<Scheduler
collisionStrategy="swap"
onAppointmentMove={(appointment, sourceLaneId, targetLaneId, newStartSlot, details) => {
if (details.operation === "swap") {
// Move appointment to targetLaneId/newStartSlot and move
// details.swappedAppointment to details.swappedAppointmentNewLaneId.
return;
}
// Existing move behavior
}}
>
<Lane laneId="room-1" appointments={roomOneAppointments} />
<Lane laneId="room-2" appointments={roomTwoAppointments} />
</Scheduler>Swaps are atomic. If the displaced appointment cannot be placed in the dragged appointment's original lane and start slot because of locked, blocked slots, bounds, or other invalid overlaps, the drop is rejected.
Blocked Slots with Custom Logic
const vipAppointment = {
id: "1",
startSlot: 5,
duration: 2,
onBlockedSlot: (slotIndex, laneId) => {
// Return true to allow VIP appointments on blocked slots
return true;
},
};Styling
The component uses Tailwind CSS internally. To customize colors and visual configuration:
<Lane
config={{
slotColor: "#f9fafb",
slotBorderColor: "#e5e7eb",
height: 100,
slotWidth: 80,
}}
/>Custom Element ClassNames
Customize the appearance of appointment containers and resizers with your own CSS classes:
<Lane
laneId="room-1"
appointments={appointments}
appointmentContainerClassName="border-2 border-blue-600 rounded-lg"
appointmentResizerStartClassName="bg-green-500 opacity-80"
appointmentResizerEndClassName="bg-red-500 opacity-80"
appointmentResizerStartInnerClassName="bg-green-700 w-1"
appointmentResizerEndInnerClassName="bg-red-700 w-1"
/>Note: Custom classes are intelligently merged with default classes. Classes with conflicting properties (e.g., bg-*, border-*, w-*) will override the defaults while preserving non-conflicting styles.
Each resizer element has two levels:
- Outer container (
appointmentResizerStartClassName/appointmentResizerEndClassName): The larger hitbox for easier grabbing - Inner line (
appointmentResizerStartInnerClassName/appointmentResizerEndInnerClassName): The visible line within the container
TypeScript
Full TypeScript support included:
import type {
Appointment,
LaneConfig,
LaneProps,
} from "@pangeasi/lane-scheduler";License
MIT
