npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-calendar

Requires 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