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

@wts-calendar/core

v1.1.1

Published

A lightweight, framework-agnostic JavaScript calendar package for event management and responsive scheduling.

Readme

WTS Calendar

A framework-agnostic TypeScript calendar for event and resource scheduling.

Provider credentials and API-key setup are documented separately in API keys, OAuth, and browser credentials. For paid modules, see how to request and use a premium license.

Install

npm install @wts-calendar/core
import { WtsCalendar, type CalendarOptions } from '@wts-calendar/core';

const container = document.querySelector<HTMLElement>('#calendar');

if (!container) {
  throw new Error('Calendar container was not found.');
}

const options: CalendarOptions = {
  container,
  view: 'month',
  viewDate: new Date(),
  events: [
    {
      id: 'planning',
      title: 'Planning',
      start: new Date(2026, 7, 4, 10),
      end: new Date(2026, 7, 4, 11),
    },
  ],
};

const calendar = new WtsCalendar(options);

const unsubscribe = calendar.on('event-click', ({ source }) => {
  console.log(source);
});

// Framework component cleanup:
unsubscribe();
calendar.destroy();

The standard entry point includes month and DayGrid views. Day/Week TimeGrid, MultiMonth/year, agenda/list, external drag-and-drop, and larger specialist features are optional subpath modules in the same npm package:

Import the production stylesheet once before constructing a calendar:

import '@wts-calendar/core/styles/calendar.css';
import { WtsCalendar } from '@wts-calendar/core';
import { rrulePlugin } from '@wts-calendar/core/rrule';
import { icalendarPlugin } from '@wts-calendar/core/icalendar';
import { googleCalendarPlugin } from '@wts-calendar/core/google-calendar';
import { interactionModule } from '@wts-calendar/core/interaction';
import { timeGridModule } from '@wts-calendar/core/time-grid';
import { multiMonthModule } from '@wts-calendar/core/multi-month';
import { listModule } from '@wts-calendar/core/list';
import { resourceSchedulingModule } from '@wts-calendar/core/resource-scheduling';
import { AdvancedResourcePlanner } from '@wts-calendar/core/advanced-resource-planning';
import { PremiumCalendarInteroperability } from '@wts-calendar/core/premium-interoperability';
import { EnterpriseCalendarWorkflow } from '@wts-calendar/core/enterprise-workflow';
import { CalendarDevTools } from '@wts-calendar/core/developer-tools';
import { CalendarTestDriver } from '@wts-calendar/core/testing';
import { CalendarDataClient } from '@wts-calendar/core/data-adapter-sdk';
import { createCalendarEventEditor } from '@wts-calendar/core/event-editor';
import { repeatedTasksModule } from '@wts-calendar/core/repeated-tasks';

const calendar = new WtsCalendar({
  ...options,
  plugins: [
    interactionModule,
    timeGridModule,
    multiMonthModule,
    listModule,
    resourceSchedulingModule,
    repeatedTasksModule,
    rrulePlugin,
    icalendarPlugin,
    googleCalendarPlugin,
  ],
});

Import only the integrations the application uses. Simple object-based recurrence remains part of core. The optional subpaths bundle their implementations, so consumers do not install rrule or ical.js separately. Applications that import a recurring .ics feed should configure both icalendarPlugin and rrulePlugin.

For a showcase, prototype, or application that needs every built-in feature, use the convenience entry point:

import { WtsCalendar } from '@wts-calendar/core/all';

/all configures interaction, MultiMonth/year, Day/Week TimeGrid, list, resource scheduling, repeated-task views, RRULE, iCalendar, and Google Calendar support for each instance. It does not perform global registration. For production bundles, prefer the standard entry plus only the modules the application uses.

Third-party npm packages can also export arbitrary CalendarExternalPlugin objects. They may register custom view engines and namespaced options with core-owned view lifecycle cleanup. The optional @wts-calendar/core/plugin-sdk entry adds dependency/conflict resolution, refined options, composable event transforms, toolbar actions, and installation lifecycle without charging that orchestration to the standard bundle. See Third-party plugin SDK. Plugin authors can copy the private reference package in projects/wts-calendar-plugin-starter and run its packed-artifact plugin:verify conformance gate before publication.

Package structure

| Import | Contents | | --- | --- | | @wts-calendar/core | Calendar core, DayGrid/month views, event APIs, sources, selection, time zones, and licensing | | @wts-calendar/core/native | DOM-free controller, recurrence, navigation, event CRUD, and snapshots used by @wts-calendar/react-native | | @wts-calendar/core/time-grid | Day, week, and custom TimeGrid views | | @wts-calendar/core/multi-month | MultiMonth, year, and custom multi-month views | | @wts-calendar/core/list | List day/week/month/year and custom list views | | @wts-calendar/core/interaction | External and cross-calendar drag-and-drop plus makeDraggable | | @wts-calendar/core/resource-scheduling | Resource grid, resource day/week time grids, resource timeline, and non-resource timeline | | @wts-calendar/core/advanced-resource-planning | Premium capacity heatmaps, shifts/rotations, dependencies, substitutes, overbooking, forecasting, and critical paths | | @wts-calendar/core/premium-interoperability | Premium Google, Microsoft 365, CalDAV, ICS reconciliation, date-format migration, and FullCalendar migration toolkit | | @wts-calendar/core/enterprise-workflow | Premium approvals, state machines, audit history, field policies, offline queues, and customer backend adapters | | @wts-calendar/core/developer-tools | Free typed data, headless validation/schema, diagnostics, DevTools, profiling, and theme generation | | @wts-calendar/core/testing | Free deterministic fixtures, drivers, mocks, idle helpers, and assertions | | @wts-calendar/core/data-adapter-sdk | Free cached/retrying REST, GraphQL, storage, and WebSocket adapter toolkit | | @wts-calendar/core/event-editor | Free accessible create/edit/duplicate/delete dialog or drawer with validation and persistence hooks | | @wts-calendar/core/repeated-tasks | Monthly and weekly repeated-task views | | @wts-calendar/core/rrule | Advanced RFC RRULE recurrence | | @wts-calendar/core/icalendar | .ics import and export | | @wts-calendar/core/google-calendar | Public API-key and private OAuth Google Calendar adapter | | @wts-calendar/core/format-moment | Optional Moment string-format compatibility plugin | | @wts-calendar/core/format-luxon3 | Optional Luxon 3 string-format compatibility plugin | | @wts-calendar/core/plugin-sdk | Third-party ecosystem orchestration and lifecycle | | @wts-calendar/core/web-component | Standards-based custom-element adapter | | @wts-calendar/core/styles/calendar.css | Required standard-view stylesheet | | @wts-calendar/core/all | Core plus every built-in optional module |

React Native

Install the official native renderer with the same core package:

npm install @wts-calendar/core @wts-calendar/react-native
import { WtsCalendarNative } from '@wts-calendar/react-native';

<WtsCalendarNative
  initialOptions={{ view: 'month', firstDay: 1 }}
  events={[{ id: 'launch', title: 'Launch', start: '2026-09-10T10:00:00' }]}
/>;

This renders native iOS/Android controls for month, week, day, and virtualized list views. It does not create another core package, mount HTML, or use a WebView.

AdvancedResourcePlanner is protected by the independent signed advanced-resource-planning entitlement. It operates entirely on the runtime snapshot supplied by the application and does not require or contact a WTS backend. See Advanced resource planning.

PremiumCalendarInteroperability is protected by the independent signed premium-interoperability entitlement. It provides runtime-only provider adapters and migration tools and is deliberately not auto-loaded by /all. See Premium interoperability.

EnterpriseCalendarWorkflow is protected by the independent signed enterprise-workflow entitlement. It supplies runtime-only state and approval governance, optimistic/offline mutation handling, hash-chained audit evidence, and a transport-neutral adapter for customer systems. It is deliberately not auto-loaded by /all and does not require a WTS backend. See Enterprise workflow.

The developer entries are normal/free and deliberately absent from both the standard and /all production graphs. See Developer tools, Accessible event editor, Testing toolkit, and Data adapter SDK.

Framework adapters

The official adapters preserve the same WtsCalendar instance and public API instead of recreating or hiding the core controller:

npm install @wts-calendar/core @wts-calendar/react
# or: @wts-calendar/vue / @wts-calendar/angular

| Framework | Package | Live update contract | | --- | --- | --- | | Angular 17-22 | @wts-calendar/angular | Standalone component, typed inputs/outputs, getApi(), zoneless and SSR-safe lifecycle | | React 18-19 | @wts-calendar/react | Ref-based getApi(), mount-only initial options, transactional prop updates | | Vue 3.4+ | @wts-calendar/vue | Exposed getApi(), shallow core instance, watched option/data props |

Each adapter accepts construction-only initialOptions, live dynamic options, and focused events and resources collections. All three are tested against the same update and teardown contract in Chromium, Firefox, WebKit, and touch Chromium. See each adapter package README for its native template/JSX example.

All official adapters also accept the typed CalendarFactory construction hook. Pass createPluginCalendar from @wts-calendar/core/plugin-sdk to enable the same advanced third-party plugin lifecycle in Angular, React, Vue, or an individual Web Component instance.

Views

Core: month, day-grid-week, and day-grid-day. Optional view modules add day, week, multi-month, year, list-day, list-week, list-month, list-year, list, timeline, resource, and repeated-task views.

Declarative toolbars

Use headerToolbar and footerToolbar to compose navigation without building or wiring custom DOM. Commas join adjacent buttons into one group while spaces create a visual gap. Sections accept built-in actions (prev, next, prevYear, nextYear, today, and title), standard or named view names, and keys from buttons, customButtons, or toolbarElements.

const calendar = new WtsCalendar({
  container,
  view: 'month',
  headerToolbar: {
    start: 'prevYear,prev next,nextYear today',
    center: 'title',
    end: 'month,week,day list-month exportRange syncStatus',
  },
  footerToolbar: {
    start: 'day-grid-week',
    end: 'exportRange',
  },
  customButtons: {
    exportRange: {
      text: 'Export',
      hint: 'Export the visible calendar range',
      click: (_event, button) => {
        button.setAttribute('aria-busy', 'true');
        exportVisibleRange(calendar).finally(() => {
          button.removeAttribute('aria-busy');
        });
      },
    },
  },
  toolbarElements: {
    syncStatus: 'Synced',
  },
  buttonDisplay: 'icon-text',
  headingLevel: 2,
  buttonClass: ({ name, isSelected, isDisabled }) => [
    `calendar-action-${name}`,
    isSelected ? 'is-selected' : '',
    isDisabled ? 'is-disabled' : '',
  ],
  buttonText: {
    'list-month': 'Agenda',
  },
  buttonHints: {
    week: 'Open weekly schedule',
  },
});

left and right are supported aliases for start and end. The active view button exposes aria-pressed="true", titles and navigation state remain synchronized in both toolbars, and every control has an accessible label. Set either toolbar to false to suppress it. Omitting headerToolbar keeps the existing package header for backward compatibility. toolbarClass, headerToolbarClass, footerToolbarClass, toolbarSectionClass, toolbarTitleClass, buttonGroupClass, and buttonClass expose typed render information. Toolbar structure is validated before the host DOM is changed; all toolbar options are transactionally updateable through setOption or setOptions and are restored by CalendarOptionChange.revert().

Sizing and sticky regions

The complete sizing family is runtime-updateable. Numeric sizes are pixels; strings accept auto or any valid CSS size.

const calendar = new WtsCalendar({
  container,
  height: 'min(820px, 90vh)',
  contentHeight: 680,
  aspectRatio: 1.35, // used when height/contentHeight are omitted
  expandRows: true,
  tableHeaderSticky: 'auto',
  footerScrollbarSticky: true,
});

calendar.setOption('contentHeight', 720);
calendar.updateSize(); // call after an external layout transition if needed

height includes toolbars. contentHeight sizes only the active view body and takes precedence over aspectRatio. Sticky table headers activate automatically for constrained calendars. The footer scrollbar mirrors the active horizontal timeline/resource scroller and supports two-way scrolling.

Themes and color schemes

standard preserves the existing package appearance. The stock classic, monarch, forma, breezy, and pulse themes share one documented token contract and work with light, dark, or OS-driven auto color schemes.

calendar.setOptions({
  theme: 'breezy',
  colorScheme: 'auto',
  themeTokens: {
    primary: '#0ea5e9',
    primaryForeground: '#082f49',
    surface: '#ffffff',
    border: '#bae6fd',
    borderRadius: '12px',
  },
});

The public CSS custom properties use the --wts-calendar-* prefix, including background, surface, surface-muted, text, text-muted, border, primary, primary-foreground, today-background, selection-background, event-background, event-text, shadow, font-family, and border-radius. Inline themeTokens win over stock palettes; pre-existing host inline styles and variables are restored when the calendar is destroyed.

Named custom views

Use views to define application-specific durations without duplicating a view engine. DayGrid, TimeGrid, list, timeline, and multi-month definitions reuse the normal event sources, recurrence, valid ranges, selection, interaction, render hooks, accessibility, and lifecycle behavior.

const calendar = new WtsCalendar({
  container,
  view: 'three-day',
  views: {
    'three-day': {
      type: 'time-grid',
      duration: { days: 3 },
      dateIncrement: { days: 3 },
      dateAlignment: 'day',
      buttonText: '3 day',
    },
    fortnight: {
      type: 'day-grid',
      duration: { weeks: 2 },
      dateIncrement: { weeks: 2 },
      dateAlignment: 'week',
    },
  },
  events,
});

calendar.setView('fortnight', '2026-08-03');

Each duration must contain exactly one positive days, weeks, months, or years field. Multi-month definitions accept only months or years.

Multi-month and year planner

multi-month renders a configurable rolling range; year renders January through December of the active year. Both reuse month-view events, recurrence, selection, drag, resize, valid ranges, hidden days, event sources, render hooks, keyboard interaction, and accessibility behavior.

import { multiMonthModule } from '@wts-calendar/core/multi-month';

const calendar = new WtsCalendar({
  container,
  plugins: [multiMonthModule],
  view: 'multi-month',
  viewDate: '2026-08-03',
  multiMonth: {
    durationMonths: 3,
    columns: 'auto',
    minimumMonthWidth: 280,
    gap: 16,
    virtualizationThreshold: 6,
  },
  events,
});

calendar.setView('year', '2026-01-01');

With automatic columns, panels reflow to the available width and collapse to one column on narrow screens. columns also accepts a fixed integer from 1 to 6. Larger planners use browser render virtualization while retaining their full accessible DOM and scroll range.

Repeated tasks module: monthly-repeated-task and weekly-repeated-task.

The monthly view treats task end dates as inclusive, clips ranges that cross month boundaries, and keeps one expanded month at a time. Calling the addTask or updateTask function supplied by a day-click or event-click payload updates the task model before rerendering, so the change survives navigation and calendar.render().

The weekly view renders one task-category row across the active week. It honors startOfWeek, weekends, hiddenDays, configured off-days, disabled past/future dates, multi-day occurrences, and allowOverlap. Each occurrence keeps its status and stable ID, and the same mutation helpers are available from its day-click and event-click payloads. Arrow keys move between interactive task cells. The grid can be themed without overriding structural styles:

.wts-calendar {
  --weekly-task-label-width: 220px;
  --weekly-task-day-min-width: 132px;
  --weekly-task-row-min-height: 96px;
  --weekly-task-border-color: #d8dee8;
  --weekly-task-today-surface: #eef7ff;
  --weekly-task-offday-surface: #fff4f1;
}
const tasks = calendar.getTasks();
const focusTask = calendar.getTaskById('focus-time');

const added = calendar.addTask('focus-time', {
  id: 'focus-2026-08-04',
  start: '2026-08-04T09:00:00',
  end: '2026-08-04T11:00:00',
  reason: 'Release work',
  status: 'pending',
});

calendar.setTaskStatus(
  'focus-time',
  added.event!.id,
  'completed',
);
calendar.updateTask('focus-time', added.event!.id, {
  reason: 'Release work completed',
});
calendar.removeTask('focus-time', added.event!.id);

calendar.on('day-click', ({ source }) => {
  source.addTask({
    task: source,
    event: {
      start: source.date,
      reason: 'Completed',
    },
  });
});

calendar.on('task-add', async (transaction) => {
  await saveTaskOccurrence(
    transaction.task.id,
    transaction.event,
  );
});

calendar.on('task-update', (transaction) => {
  return updateTaskOccurrence(
    transaction.task.id,
    transaction.oldEvent,
    transaction.event,
  );
});

Every task category and occurrence has a stable id. Occurrence status is one of pending, completed, late, missed, skipped, or excused. Occurrence IDs must be unique within their category. Same-category date ranges cannot overlap unless that category explicitly sets allowOverlap: true; forceOverride: true replaces conflicting occurrences during a mutation. Snapshots returned by getTasks() and getTaskById() are cloned and frozen. Task mutations are optimistic transactions: returning false, rejecting a promise, or calling transaction.revert() restores the previous task state when no newer mutation made the transaction stale. Lifecycle events include task-add, task-update, task-remove, task-status-change, task-change-pending, task-change-settled, task-change-error, and task-revert.

Task icons may be plain text or inline SVG. SVG scripts, external references, event-handler attributes, and embedded HTML are removed before rendering.

Async task sources

Repeated-task categories can come from static task, one or more taskSources, or both. Every source load receives the visible range, active view, time zone, abort signal, source ID, and load reason. Navigation aborts obsolete requests and sequence protection ignores stale loaders that do not honor the signal.

const calendar = new WtsCalendar({
  container,
  license,
  plugins: [repeatedTasksModule],
  view: 'monthly-repeated-task',
  task: localTasks,
  taskSources: [{
    id: 'work-tracker',
    url: '/api/calendar/tasks',
    request: { pageSize: 100 },
    requestAdapter: ({ start, end, timeZone, request }) => ({
      from: start,
      to: end,
      timeZone,
      cursor: request.cursor,
      limit: request.pageSize,
    }),
    fetchOptions: async () => ({
      headers: {
        authorization: `Bearer ${await getAccessToken()}`,
      },
    }),
    responseAdapter: response => ({
      tasks: response.data.categories,
      meta: { revision: response.data.revision },
      page: {
        nextCursor: response.data.nextCursor,
        hasMore: Boolean(response.data.nextCursor),
        total: response.data.total,
      },
    }),
    taskDataTransform: category => ({
      id: category.id,
      version: category.version,
      name: category.code,
      title: category.label,
      enable: category.active,
      icon: category.icon ?? '',
      data: category.occurrences.map(occurrence => ({
        id: occurrence.id,
        version: occurrence.version,
        start: occurrence.start,
        end: occurrence.end,
        reason: occurrence.note,
        status: occurrence.status,
      })),
    }),
    retries: 2,
    retryDelay: attempt => attempt * 500,
  }],
});

Without responseAdapter, an endpoint may return a task array directly or:

{
  "tasks": [
    {
      "id": "focus-time",
      "name": "focus-time",
      "title": "Focus time",
      "enable": true,
      "icon": "",
      "data": [
        {
          "id": "focus-2026-08-04",
          "start": "2026-08-04",
          "status": "completed"
        }
      ]
    }
  ],
  "meta": {
    "revision": 17
  }
}

Source operations are addTaskSource(source), removeTaskSource(id), getTaskSources(), refetchTasks(sourceId?, request?), and loadMoreTasks(sourceId, request?). refetchSources() also refreshes task sources. Loads emit task-source-loading, task-source-success, and task-source-error.

Each successful response replaces only that source's owned categories as one atomic snapshot. Static tasks and other source snapshots remain intact. Invalid or failed refreshes retain the previous successful data. invalidTaskPolicy: 'skip' keeps valid categories and reports invalid records through skippedTasks; the default is reject-source. Local task mutations update the owning static or source snapshot, while a later successful refetch reconciles that source to the server's stable category and occurrence IDs. Cursor and page-based pagination append new categories and merge additional occurrences into an existing category by its stable ID. Optional string or numeric version fields are preserved on both categories and occurrences so mutation callbacks can send optimistic-concurrency values back to the application server.

Resource scheduling module: resource, resource-day-grid-day, resource-day-grid-week, resource-time-grid-day, resource-time-grid-week, and resource-timeline. Vertical resource grids support datesAboveResources for date-major versus resource-major headings.

Agenda/list views

The list views render foreground events in accessible date groups:

  • list-day shows one calendar day.
  • list-week follows startOfWeek.
  • list-month shows the exact calendar month.
  • list-year shows the exact calendar year.
  • list shows a custom number of days configured with listView.durationDays.
const calendar = new WtsCalendar({
  container,
  view: 'list',
  viewDate: '2026-08-03',
  listView: {
    durationDays: 14,
    virtualizationThreshold: 200,
    overscan: 8,
    showEmptyDays: false,
    dayFormat: 'EEEE, MMMM d, yyyy',
    eventTimeFormat: 'HH:mm',
    stickyHeaders: true,
  },
  events,
});

calendar.setView('list-month', '2026-09-01');

Recurring events and occurrence overrides expand through the same recurrence and named-time-zone pipeline as grid views. Advanced RRULE expansion is available when rrulePlugin is configured. Multi-day events appear in every visible date group they intersect. Background and inverse-background events remain availability layers and are intentionally omitted from the agenda.

List rows support event clicks, event render hooks, Arrow Up/Down and Home/End navigation, resource labels and filtering, hidden days, valid ranges, loading and error announcements, and an empty state. Once the configured threshold is reached, measured variable-height rows are virtualized with overscan; only the visible window is mounted while the full scroll range and keyboard positions remain available. noEventsContent customizes the empty range. When licensed resource scheduling is configured, listView.resourceIds filters the agenda to assigned events and displays the matching resource titles.

External and cross-calendar drag

Set droppable: true on a receiving calendar. Bind backlog, kanban, or task elements with makeDraggable; the returned cleanup function removes every listener and restores the element attributes.

const calendar = new WtsCalendar({
  container,
  view: 'month',
  droppable: true,
  dropAccept: '.calendar-backlog-item',
  eventTransferMode: 'move',
  eventOverlap: false,
  eventDragMinDistance: 6,
  eventLongPressDelay: 500,
  dragRevertDuration: 250,
  dragScroll: true,
  drop: ({ draggedEl, dateStr }) => {
    console.log('External element reached', dateStr, draggedEl);
  },
});

const backlogItem = document.querySelector<HTMLElement>('#backlog-item')!;
const stopDragging = calendar.makeDraggable(backlogItem, {
  title: backlogItem.dataset['title'] ?? 'Backlog item',
  durationMinutes: 60,
}, {
  minDistance: 6,
  longPressDelay: 500,
  appendTo: document.body,
});

calendar.on('event-receive', async (transaction) => {
  const response = await fetch('/api/calendar/events', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(transaction.event),
  });
  return response.ok; // false or rejection rolls the drop back automatically
});

// Component teardown:
stopDragging();
calendar.destroy();

For delegated HTML drag sources, use externalEventData instead of binding each element:

const calendar = new WtsCalendar({
  container,
  droppable: true,
  externalEventData: ({ element }) => ({
    title: element.dataset['title'] ?? 'Untitled event',
    durationMinutes: Number(element.dataset['duration'] ?? 60),
  }),
});

Dragging an event into another droppable WtsCalendar moves it by default. Set eventTransferMode: 'copy' on the source calendar, or pass { mode: 'copy' } to makeDraggable, to copy instead. Cross-calendar moves preserve the source event identity; copies receive a fresh identity.

The receiving calendar emits event-receive once for every accepted external or cross-calendar event. The drop callback always runs for an accepted external element, even when its event data specifies create: false; in that case event-receive does not run. The calendar additionally emits the legacy external-drop event for a created external event or event-transfer for a calendar transfer. A moved source emits event-leave. Transaction callbacks may return false, reject, or call transaction.revert(). Valid range, hidden-day, business-hour, eventConstraint, eventOverlap, eventAllow, resource, and capacity checks run before callbacks. dropAccept accepts a CSS selector or a predicate receiving the external element.

Pointer and touch input use an event-only preview without highlighting cells. For keyboard input, focus a bound external item and press Space, use the arrow keys to choose a target, then Enter to commit or Escape to cancel. On an event inside a calendar, Shift+Space starts a cross-calendar keyboard transfer.

Standard and premium licensing

Month, week, day, and agenda/list views are standard features. Their event APIs, event sources, named time zones, and rendering hooks work without a license. Grid views additionally provide selection and drag/resize interactions.

Resource scheduling, including resource, resource-day-grid-day, resource-day-grid-week, resource-time-grid-day, resource-time-grid-week, and resource-timeline, and the monthly/weekly repeated-task views are premium features. All resource views use the existing resource-scheduling license feature; they do not require separate grants. Premium access uses a signed Ed25519 license token rather than a plain API key. To obtain one, contact the maintainer using the steps in Premium licensing. That guide lists the information to provide, the available entitlement names, and safe browser-only delivery.

The signed audience remains wts-calendar-v2 as a stable entitlement protocol identifier after migration to the @wts-calendar/core npm name. This preserves existing customer grants; the audience is not an import or package name and must not be mechanically rewritten.

Example:

import {
  WtsCalendar,
  verifyCalendarLicense,
} from '@wts-calendar/core';
import {
  resourceSchedulingModule,
} from '@wts-calendar/core/resource-scheduling';

// Supply the signed token at runtime. It may come from a customer-controlled
// backend or runtime configuration; never commit a production token.
const license = await verifyCalendarLicense(runtimeConfig.wtsCalendarLicense);

const calendar = new WtsCalendar({
  container,
  license,
  plugins: [resourceSchedulingModule],
  view: 'resource',
  resources: [
    { id: 'design', title: 'Design team' },
  ],
  events: [
    {
      title: 'Design review',
      start: '2026-08-04T10:00:00',
      resourceId: 'design',
    },
  ],
});

console.log(calendar.getLicenseStatus());

Resource DayGrid

Use resource-day-grid-day or resource-day-grid-week for resource columns without a time axis. Timed and all-day events share the resource/date cell. datesAboveResources: false (the default) groups columns by resource first; set it to true to group by date first. The same heading option applies to Resource TimeGrid.

Resource day and week time grids

Use resource-time-grid-day or resource-time-grid-week to display resources as columns in a timed schedule. Both views require at least one configured resource or resource source. An event is placed in a resource column through its resourceId:

const calendar = new WtsCalendar({
  container,
  license,
  view: 'resource-time-grid-week',
  viewDate: '2026-08-03',
  resources: [
    { id: 'design', title: 'Design' },
    { id: 'engineering', title: 'Engineering' },
  ],
  weekView: {
    hourSegment: 30,
    segmentHeight: 40,
    dragTolerance: 6,
    resizeTolerance: 1,
  },
  resourceTimeGrid: {
    columnWidth: 168,
    virtualizationThreshold: 24,
    overscan: 2,
  },
  columnResizing: {
    enabled: true,
    minWidth: 72,
    maxWidth: 480,
    keyboardStep: 12,
  },
  selectable: true,
  events: [
    {
      id: 'review',
      title: 'Design review',
      start: '2026-08-04T10:00:00',
      end: '2026-08-04T11:00:00',
      resourceId: 'design',
    },
  ],
});

calendar.on('select', ({ start, end, resourceId }) => {
  console.log({ start, end, resourceId });
});

calendar.setView('resource-time-grid-day', '2026-08-04');

The day form reuses dayView options and the week form reuses weekView options, including hourSegment, hourSegmentRange, segmentHeight, dragTolerance, and resizeTolerance. Global slotMinTime, slotMaxTime, scrollTime, business hours, valid ranges, and named time-zone behavior also apply.

Editable timed events can be moved to another time or resource column and resized using pointer, touch, or keyboard input. A committed move updates start, preserves the event duration, and sets the destination resourceId; a committed resize updates end without changing resourceId. Selection is timed and returns the selected column's resourceId. The normal event-drop, event-resize, constraint, pending-validation, rollback, and selection APIs are used.

Resource columns have a configurable fixed width and the view uses horizontal overflow when all columns do not fit. Once the flattened date × resource column count reaches virtualizationThreshold, only the visible columns plus overscan are mounted. Headers, all-day cells, timed cells, event hooks, selection focus, and scroll geometry remain synchronized. Set a higher threshold to keep small schedules fully mounted. The existing all-day resource view retains its independent row-virtualization behavior.

Resources with parentId are rendered as an expandable hierarchy in both resource time-grid modes and in the all-day resource view. Collapsing a parent removes its descendant columns or rows, including descendant events, without changing the event or resource data. Expansion state survives date navigation and dynamic option rebuilds. Toggle buttons support click, touch, Enter/Space, and Arrow Left/Right interaction. The resourceTimeline.resourcesInitiallyExpanded option supplies the initial state consistently across every resource scheduling view.

Collapsed parents retain a text summary of the hidden subtree. The summary contains the visible-range event count, peak concurrent resourceUnits, the sum of leaf-resource capacity, utilization, unavailable assignments, and capacity conflicts. The same calculation is used by Resource, Resource Time Grid, and Resource Timeline, including their virtualized layouts.

const calendar = new WtsCalendar({
  container,
  license,
  view: 'resource-timeline',
  resources,
  events,
  resourceTimeline: {
    resourcesInitiallyExpanded: false,
  },
  resourceSummaryContent: ({ summary }) =>
    `${summary.eventCount} scheduled · ` +
    `${summary.bookedUnits}/${summary.capacity ?? '—'} units` +
    (summary.conflictCount ? ` · ${summary.conflictCount} conflicts` : ''),
});

Summary content is text-only. Each collapsed row or column also exposes data-calendar-resource-event-count, data-calendar-resource-booked-units, data-calendar-resource-capacity, and data-calendar-resource-conflict-count for application styling and testing. Use --calendar-resource-summary-* CSS variables to customize its typography and conflict colors.

Resource timeline

Use timeline for the same horizontal date scale without resource rows. It renders every ordinary event in one virtualized lane stack and does not require resources or assign a synthetic resource ID to callbacks:

import { WtsCalendar } from '@wts-calendar/core';
import { resourceSchedulingModule } from
  '@wts-calendar/core/resource-scheduling';

const calendar = new WtsCalendar({
  container,
  license,
  plugins: [resourceSchedulingModule],
  view: 'timeline',
  viewDate: '2026-08-03',
  events,
  resourceTimeline: { durationDays: 30, slotWidth: 88 },
});

Named timeline durations are also supported through views. Timeline remains part of the optional resource-scheduling entry point because both layouts share the same two-axis scale, interaction, and virtualization engine.

Use resource-timeline for a multi-day plan with resources as rows and days as horizontal slots:

const calendar = new WtsCalendar({
  container,
  license,
  view: 'resource-timeline',
  viewDate: '2026-08-03',
  resources: [
    {
      id: 'delivery',
      title: 'Product delivery',
      capacity: 12,
      extendedProps: { location: 'Global' },
    },
    {
      id: 'design',
      parentId: 'delivery',
      title: 'Design',
      capacity: 4,
      extendedProps: { location: 'London' },
    },
    {
      id: 'engineering',
      parentId: 'delivery',
      title: 'Engineering',
      capacity: 8,
      extendedProps: { location: 'Remote' },
    },
  ],
  resourceTimeline: {
    durationDays: 30,
    slotWidth: 88,
    rowMinHeight: 52,
    resourceAreaWidth: 220,
    resourceVirtualizationThreshold: 50,
    slotVirtualizationThreshold: 60,
    resourceOverscan: 6,
    slotOverscan: 3,
    resourcesInitiallyExpanded: true,
    resourceAreaColumns: [
      {
        field: 'title',
        header: 'Team',
        width: 220,
        minWidth: 160,
        maxWidth: 360,
      },
      {
        field: 'capacity',
        header: 'Capacity',
        width: 96,
        resizable: false,
      },
      { field: 'location', header: 'Location', width: 120 },
      {
        field: 'summary',
        header: 'Summary',
        width: 140,
        value: (resource) => `${resource.capacity ?? 0} seats`,
      },
    ],
  },
  selectable: true,
  events: [
    {
      id: 'launch',
      title: 'Launch preparation',
      start: '2026-08-04',
      end: '2026-08-08',
      isAllDay: true,
      resourceId: 'design',
      editable: true,
    },
  ],
});

The timeline supports deterministic overlap lanes, foreground and background events, business and valid-range shading, resource-scoped selection, cross-resource/date dragging, end-date resizing, lifecycle hooks, asynchronous interaction validation, and named time zones. Drag and resize are available through pointer, touch, and keyboard input.

Resource rows and day slots virtualize independently once their configured thresholds are reached. Sticky resource names, date headers, focus restoration, selection state, and balanced eventDidMount/eventWillUnmount hooks remain synchronized as either axis scrolls. durationDays accepts 1–366 calendar days; navigation advances by the configured duration.

Set parentId to build any validated, cycle-free resource hierarchy shared by the all-day resource grid, resource day/week time grids, and resource timeline. Expandable rows and columns retain their state across navigation and dynamic option rebuilds and expose tree semantics for assistive technology. resourceAreaColumns accepts built-in title and capacity fields, exact extendedProps keys, or a text-only value callback. Values are assigned with textContent; HTML column values are not accepted. A trusted content render hook may instead return text or DOM nodes. Explicit column widths are combined into the sticky resource area. When columns are omitted, resourceAreaWidth continues to control the single resource-title column.

Resource visibility and ordering are independent from the source collection. resourceFilter receives the resource, its assigned events, the active view, event presence, and descendant-match count. Matching descendants retain their ancestors, so a filter cannot create an invalid tree. Set filterResourcesWithEvents for the common event-presence filter. Source order is preserved unless resourceOrder supplies comma-separated fields, descending -field entries, comparator functions, or a mixed array. None of these operations mutate the application's resource array.

Resource Timeline supports both the compatible resourceGroupField shortcut and nested resourceGroupFields. Group tiers accept derived-value callbacks, labels, empty labels, and independent ordering. Setting group: true or a group definition on a resource-area column adds that column as an outer group tier. Column groups, nested global groups, and the existing parentId hierarchy can be used together:

const calendar = new WtsCalendar({
  container,
  license,
  plugins: [resourceSchedulingModule],
  view: 'resource-timeline',
  resources,
  events,
  resourceFilter: ({ resource }) => resource.extendedProps?.active !== false,
  resourceOrder: ['-capacity', 'title'],
  resourceGroupOrder: 'asc',
  resourceGroupFields: [
    { field: 'region', label: 'Region', order: 'desc' },
    {
      field: 'utilizationBand',
      value: (resource) => Number(resource.capacity ?? 0) >= 10
        ? 'Large'
        : 'Small',
    },
  ],
  resourceTimeline: {
    resourceAreaColumns: [
      { field: 'team', header: 'Team', group: true },
      {
        field: 'title',
        header: 'Resource',
        classNames: ({ resource }) =>
          resource?.extendedProps?.active === false ? 'is-inactive' : [],
        didMount: ({ el }) => observeResourceCell(el),
        willUnmount: ({ el }) => unobserveResourceCell(el),
      },
    ],
  },
});

Resource rendering uses balanced lifecycle hooks across ordinary rendering, virtual-window recycling, filtering, grouping, collapse/expand, navigation, option rebuilds, and destruction. The public surfaces are:

  • Column-header class/content/lifecycle, plus header-inner, divider, and header-row classes.
  • Resource-cell class/content/lifecycle, plus row, indent, and expander classes.
  • Resource-lane class/content/lifecycle, plus lane-top and lane-bottom class/content hooks.
  • Group-header and group-lane class/content/lifecycle, plus their inner classes.
  • Vertical Resource TimeGrid day-header and lane classes/content/lifecycle.
  • Per-column class, content, mount, and unmount hooks that compose with global cell hooks.

Every render info object identifies the view, resource or group, depth, field, field value, optional date, virtualization status, and virtual index. A group info object additionally exposes its stable key, field, raw value, display text, nesting level, parent key, and complete descendant resource count.

Column resizing is enabled by default in resource scheduling views. It is applied where changing width does not break scheduling geometry:

  • Resource Timeline metadata columns resize independently. Set resizable: false, minWidth, or maxWidth on an individual resourceAreaColumns entry. Its date-slot borders resize the shared timeline scale, keeping every date, event, background, and virtual spacer aligned; these commits use columnId: 'timeline-slots'.
  • Resource Time Grid changes the shared resource-column width, keeping virtualization, headers, slots, and events aligned.
  • The all-day Resource view supports the resource-name column and individual date columns, including correct multi-day event spans.

Drag a header edge with pointer or touch, or focus its separator and press Arrow Left/Right. Shift changes the keyboard step by 4× and Escape cancels an active pointer resize. Committed changes emit one column-resize callback:

calendar.on('column-resize', ({
  view,
  columnId,
  previousWidth,
  width,
  source,
}) => {
  console.log({ view, columnId, previousWidth, width, source });
});

Widths survive navigation and re-rendering for the lifetime of the calendar instance. Set columnResizing: false to disable all handles. Customize the visible hover/focus indicator with --calendar-column-resizer-color; ordinary month/week/day columns and Resource Timeline date slots retain uniform scheduling widths while allowing the shared scale to be resized.

Resource availability and assignment policies

Availability and declarative assignment rules live on each resource. Event requirements travel with the event, so the same decision is enforced for programmatic selection and mutation, pointer/touch/keyboard drag and resize, external receive, and calendar-to-calendar transfer:

const resources = [{
  id: 'design',
  title: 'Design',
  availability: {
    workingHours: {
      daysOfWeek: [1, 2, 3, 4, 5],
      startTime: '09:00',
      endTime: '17:00',
    },
    unavailable: [{
      start: '2026-08-12T13:00:00',
      end: '2026-08-12T17:00:00',
      reason: 'Design offsite',
    }],
  },
  assignmentPolicy: {
    allowedEventTypes: ['design'],
    skills: ['figma', 'research'],
    roles: ['reviewer'],
  },
}];

const events = [{
  id: 'review',
  title: 'Design review',
  start: '2026-08-11T13:00:00',
  end: '2026-08-11T14:00:00',
  resourceId: 'design',
  resourceRequirements: {
    eventType: 'design',
    skills: ['figma'],
    roles: ['reviewer'],
  },
}];

Timed assignments must fit completely within one working interval. All-day assignments require a matching working weekday. Any intersection with an exact unavailable range is rejected. Resource cells expose data-calendar-resource-unavailable and the optional data-calendar-resource-unavailable-reason, receive aria-disabled="true", and do not expose selection or drop metadata.

Use resourceAssignmentAllow for application or server approval after the built-in synchronous rules pass. It may return a boolean, a structured decision, or a promise:

const calendar = new WtsCalendar({
  // ...
  resourceAssignmentAllow: async ({ event, resource, operation }) => {
    const response = await fetch('/api/calendar/assignments/validate', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        eventId: event.id,
        resourceId: resource.id,
        operation,
      }),
    });
    if (response.ok) return true;
    return {
      allowed: false,
      code: 'approval-required',
      message: 'Resource approval is required.',
    };
  },
});

calendar.on('resource-assignment-invalid', ({ context, decision }) => {
  console.log(context.operation, context.resource.id, decision);
});

Promise decisions use the existing pending, timeout, settled, error, and automatic rollback lifecycle. Built-in failures are reported through event-constraint-violation or select-invalid with structured codes: resource-unavailable, resource-event-type, resource-skill, and resource-role. Use transactional createEvent and editEvent when async approval is required; synchronous addEvent and updateEvent enforce the declarative resource rules.

WtsCalendar.createLicensed(options, token) is an equivalent convenience API. Tokens are verified against the public key pinned in the package and may be restricted by expiry, feature, and exact browser origin. A fabricated grant, a modified token, an expired token, or an unlicensed origin is rejected before the premium calendar mutates the DOM. Premium checks also apply to later setView, addEvent, setEvents, updateEvent, and event-source operations. The deprecated apikey option is rejected because a public API key is not a meaningful browser-side guard.

The signing private key must exist only in a secrets manager or isolated licensing service. Never place it in a browser bundle, repository, CI artifact, or customer application. To create and sign a local development key from this package directory:

npm run license:keygen
npm run license:sign -- \
  .license-private/wts-calendar-ed25519-private.pem \
  test/fixtures/license-claims.json \
  .license-private/development-license.token

The .license-private directory is ignored by version control. Replace the development signing key and token before publishing a production release. Standard and premium implementations intentionally ship together in @wts-calendar/core. Runtime verification raises the cost of casual misuse, but client-side code can always be patched by a determined attacker. Stronger commercial enforcement should therefore come from the licensing service: short-lived signed tokens, account and origin binding, controlled renewal, revocation, and purchase-entitlement checks.

Visible dates and working hours

Scheduling boundaries apply consistently to month, week, day, resource, and resource day/week time-grid views:

const calendar = new WtsCalendar({
  container,
  weekends: false,
  hiddenDays: [3], // additionally hide Wednesday; Sunday is 0
  validRange: {
    start: '2026-07-01', // inclusive
    end: '2026-10-01',   // exclusive
  },
  slotMinTime: '08:00',
  slotMaxTime: '19:00',
  scrollTime: '08:30',
  scrollTimeReset: true,
  nowIndicator: true,
  businessHours: [
    {
      daysOfWeek: [1, 2, 4, 5],
      startTime: '09:00',
      endTime: '17:30',
    },
  ],
});

hiddenDays uses weekday numbers 0 through 6. weekends: false adds Sunday and Saturday to that list. Hiding all seven weekdays is rejected. Hidden columns are removed from layout, keyboard navigation, selection, and drop targets rather than rendered as empty placeholders.

validRange.start is inclusive and validRange.end is exclusive. Dates outside it remain visible where a complete month or week grid needs context, but they are disabled and cannot be selected, dropped onto, or resized into. Previous and next controls disable at the range boundary, and asynchronous event-source requests are clipped to the valid range.

slotMinTime and slotMaxTime control the day/week time window; slotMaxTime accepts 24:00. Existing dayView.hourSegmentRange or weekView.hourSegmentRange values take precedence for backward compatibility. scrollTime selects the initial position. With scrollTimeReset: false, user scroll position is preserved when the view refreshes; the default is true.

slotDuration controls time-grid row resolution while snapDuration independently controls drag and resize snapping. slotLabelFormat, allDaySlot, and allDayText configure the time axis. Use slotEventOverlap, eventMaxStack, and eventMinHeight to control dense timed-event layout. slotLabelTiers adds independently formatted and spaced axis tiers. When eventMaxStack hides overlapping events, timeGridMoreLinkContent customizes the link and timeGridMoreLinkClick chooses an accessible popover, expansion, or an application callback.

const calendar = new WtsCalendar({
  container,
  view: 'week',
  slotDuration: 30,
  slotLabelTiers: [
    { format: 'EEE MMM d', interval: 1440 },
    { format: 'HH:mm', interval: 60 },
  ],
  eventMaxStack: 3,
  timeGridMoreLinkContent: ({ count }) => `${count} conflicts`,
  timeGridMoreLinkClick: 'popover',
});

Events without end render with defaultTimedEventDuration (60 minutes) or defaultAllDayEventDuration (one calendar day). Rendering does not mutate application data unless forceEventDuration: true is configured. Date-only starts infer all-day behavior; an explicitly timed event remains timed even when it spans midnight or a full day.

Month grids support fixedWeekCount, showNonCurrentDates, dayMaxEvents, moreLinkClick, displayEventTime, displayEventEnd, and eventTimeFormat. Overflow callbacks receive the hidden events, all events, date, count, and active view. Same-range refreshes reuse the existing month grid and only reconcile event/background layers. Run npm run benchmark:month inside the package to measure the 5,000-event baseline; pass a different event count as the script's first argument when invoking the benchmark file directly. npm run benchmark:time-grid measures a 5,000-event overlap cluster and verifies that eventMaxStack leaves only the configured visible lanes mounted; the remaining events stay virtual and are available through the accessible popover.

Set weekNumbers: true to add week numbers. weekNumberCalculation accepts 'ISO', 'local', or a callback. navLinks: true turns appropriate date and week labels into day/week navigation controls.

Shared structural hooks are available across month, week, day, and list rendering: dayCell*, dayHeader*, slotLabel*, and slotLane* class, content, mount, and unmount callbacks. Hook strings are inserted as text. DOM nodes must belong to the configured calendar document. All-day events expose an invisible end-edge resize target in day/week views, supporting pointer, keyboard, and Escape cancellation without adding a visible divider.

Overflow and current-time surfaces use the same balanced lifecycle contract: moreLinkClassNames/Content/DidMount/WillUnmount, morePopoverClassNames/Content/DidMount/WillUnmount, and nowIndicatorClassNames/Content/DidMount/WillUnmount. More-link info includes the hidden and complete event sets; popover info also exposes its anchor; now-indicator info distinguishes the time-grid line from the axis marker with isAxis and includes the resource in scheduling views.

const calendar = new WtsCalendar({
  container,
  dayMaxEvents: 3,
  nowIndicator: true,
  moreLinkClassNames: ({ count }) => [`has-${count}-hidden-events`],
  moreLinkContent: ({ count }) => `Show ${count} more`,
  morePopoverDidMount: ({ el, hiddenEvents }) => {
    console.log('Overflow opened', el, hiddenEvents);
  },
  morePopoverWillUnmount: ({ el }) => {
    console.log('Overflow closed', el);
  },
  nowIndicatorClassNames: ({ isAxis }) => [
    isAxis ? 'current-time-axis' : 'current-time-line',
  ],
});

Top-level businessHours controls visible working/non-working shading. interactionConstraints.businessHours remains the rule that rejects invalid drag, resize, and selection operations; when visible business hours are not set, the constraint schedule is also used for shading. nowIndicator renders the current time only when it is inside the active time grid and valid range.

Background events and availability

Set an event's display to background for blocked, preferred, or informational ranges. Use inverse-background to shade everything outside one or more availability intervals. Inverse events with the same groupId form one union before the complement is calculated.

const calendar = new WtsCalendar({
  container,
  events: [
    {
      id: 'maintenance',
      title: 'Maintenance',
      start: '2026-08-04T12:00:00',
      end: '2026-08-04T14:00:00',
      display: 'background',
      color: '#ef4444',
    },
    {
      id: 'design-hours',
      groupId: 'design-availability',
      title: 'Design availability',
      start: '2026-08-03T09:00:00',
      resourceId: 'design',
      display: 'inverse-background',
      recurring: {
        frequency: 'weekly',
        daysOfWeek: [1, 2, 3, 4, 5],
        startDate: '2026-08-03',
        startTime: '09:00',
        endTime: '17:00',
      },
    },
  ],
});

Background events support all-day and timed ranges, recurrence, named time zones, and global or resource-specific placement. They are visual layers: they are not focusable, draggable, clickable, or counted as collisions by event-overlap and selection constraints.

Event color remains the default layer color, but it does not become an inline background-color. Applications can override the rendered colors and opacity with inherited CSS variables:

.calendar-host {
  --calendar-background-event-color: #fee2e2;
  --calendar-background-event-opacity: 0.3;
  --calendar-inverse-background-event-color: #e5e7eb;
  --calendar-inverse-background-event-opacity: 0.2;
}

/* Optional override only for previous/next-month cells. */
.calendar-host .calendar-outside-month {
  --calendar-background-event-color: #f8fafc;
  --calendar-inverse-background-event-color: #f8fafc;
}

Rendering can be controlled globally and overridden per event. eventDisplay and an event's display accept auto, block, list-item, background, inverse-background, and none. The event-level value wins. eventColor, eventTextColor, and backgroundEventColor provide calendar-wide defaults; event color and textColor remain local overrides.

eventOrder accepts a comma-separated field list, a comparator, or an array containing either. Prefix a field with - for descending order. Custom fields may be supplied through event.meta; the default is start,-duration,allDay,title. Time grids normally prioritize chronological compaction and use the configured order to break ties. Set eventOrderStrict to preserve the configured order across an overlapping cluster.

Use the rendering and interaction hooks to customize event segments:

const calendar = new WtsCalendar({
  container,
  events,
  eventOrder: '-priority,start,title',
  eventClassNames: ({ event, isBackground }) =>
    isBackground ? ['availability-layer'] : [`event-${event.id}`],
  eventContent: ({ event, timeText }) =>
    `${timeText ? `${timeText} ` : ''}${event.title}`,
  eventDidMount: ({ el, event, isStart, isEnd }) => {
    el.dataset['analyticsEvent'] = event.id ?? '';
    el.dataset['segment'] = `${isStart}-${isEnd}`;
  },
  eventWillUnmount: ({ el }) => {
    // Release element-scoped integrations here.
  },
  eventMouseEnter: ({ el }) => {
    el.dataset['hovered'] = 'true';
  },
  eventMouseLeave: ({ el }) => {
    delete el.dataset['hovered'];
  },
  eventClick: ({ event, jsEvent }) => {
    console.log(event.id, jsEvent.type);
  },
});

eventClassNames and eventContent accept either a static value or a callback; an event's own classNames are merged first. Content applies only to foreground labels. Strings are inserted as text, never HTML; returned DOM nodes must belong to the calendar's configured document. The immutable render info includes event, view, display, timeText, isBackground, isStart, isEnd, isMirror, isPast, isFuture, and isToday.

eventDidMount and eventWillUnmount are balanced once per rendered segment, including background segments. eventClick, eventMouseEnter, and eventMouseLeave receive the event, element, native event, and active view. Changing event-rendering options through setOptions() rebuilds segments with the same unmount/mount balance, and destroy() unmounts every remaining segment.

Event API

  • addEvent(event | events)
  • setEvents(events) for atomic replacement
  • getEvents()
  • getEventById(id)
  • updateEvent(id, changes)
  • createEvent(input)
  • editEvent(id, changes, { occurrence | scope })
  • removeEventWithTransaction(id, { occurrence | scope })
  • updateRecurringEvent(id, changes, { occurrence | scope })
  • removeRecurringEvent(id, { occurrence | scope })
  • removeEvent(id)
  • removeAllEvents()
  • undo() / redo() / clearEventHistory()
  • getEventHistoryState()
  • importICalendar(source, options)
  • exportICalendar(options)
  • addEventSource(source)
  • removeEventSource(id)
  • getEventSources()
  • refetchEvents(sourceId?)
  • on(name, callback) / off(name, callback)

setEvents() validates the complete replacement before changing the store, renders once, and emits events-set. Use batchRendering(() => { ... }) to coalesce several synchronous add, update, or remove operations into one render. Use await batchRenderingAsync(async () => { ... }) when mutations span an asynchronous boundary. Returning a Promise from batchRendering() throws so an asynchronous batch cannot silently render early.

Headless event creation and editing

The package does not impose a dialog framework. Enable eventCreation to turn an accepted selection into an event-create-request; the application can open its own Angular, React, Vue, Web Component, or native dialog and then commit or cancel the request.

const calendar = new WtsCalendar({
  container,
  selectable: true,
  eventCreation: { defaultTitle: 'New event' },
  eventHistory: { limit: 100 },
});

calendar.on('event-create-request', request => {
  openEventDialog(request.suggestedEvent).then(values => {
    if (values) request.commit(values);
    else request.cancel();
  });
});

calendar.on('event-edit-request', request => {
  openEventDialog(request.event).then(values => {
    if (values) {
      request.update(values, {
        scope: request.recurring ? 'occurrence' : 'series',
      });
    } else {
      request.cancel();
    }
  });
});

calendar.on('event-remove-request', request => {
  confirmRemoval(request.event).then(confirmed => {
    if (confirmed) request.confirm();
    else request.cancel();
  });
});

Double-clicking an event or pressing Enter emits event-edit-request. Delete/Backspace emits event-remove-request. Escape cancels an outstanding request. Touch applications can use the existing event-click callback to open the same editor and call editEvent() directly.

Create, edit, and remove are optimistic transactions. Listen to event-create, event-edit, or event-remove to persist a transaction, or configure one shared validator:

const calendar = new WtsCalendar({
  container,
  eventMutationValidator: async transaction => {
    const response = await fetch('/api/calendar/events', {
      method: transaction.type === 'create'
        ? 'POST'
        : transaction.type === 'remove'
          ? 'DELETE'
          : 'PATCH',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(transaction.event),
    });
    return response.ok;
  },
});

Returning or resolving false, rejecting, or exceeding interactionValidationTimeout restores the exact pre-mutation snapshot. The lifecycle emits event-mutation-pending, event-mutation-settled, event-mutation-error, and event-mutation-revert. Each accepted create/edit/remove, drag, resize, or cross-calendar transfer creates one bounded history entry. Ctrl/Cmd+Z, Ctrl+Y, and Ctrl/Cmd+Shift+Z call the same undo/redo APIs.

Recurring requests expose three scopes:

  • occurrence writes one deterministic occurrence override.
  • future atomically splits the recurrence at the selected occurrence.
  • series changes or removes the complete series.

Future scope supports core recurrence and RRULE COUNT/UNTIL schedules, including exception partitioning. Its split is one transaction and one undo entry.

User-initiated drag, drop, and resize commits emit one update-event callback with the stored CalendarEvent. Set editable: false to disable editing or draggable: false to disable dragging for an event.

In month view, drag an event to move it while preserving its time and duration. Use the event's end edge to extend or shrink it across dates and weeks. The resize target supports pointer/touch input plus Left/Right arrows; press Enter to commit or Escape to cancel. Month resizing preserves the existing end time instead of converting it to midnight.

Editable events in month, day, week, resource, and resource time-grid views are keyboard draggable: focus an event, press Space to grab it, move with the arrow keys, then press Enter or Space to commit. In resource time grids, Up/Down changes time and Left/Right changes resource columns. Enter opens the event without starting a drag. Escape cancels without an update. Day, week, and resource time-grid resize targets use the arrow keys to adjust duration, Enter or Space to commit, and Escape to restore the original duration. Drag and resize previews animate the event itself and do not highlight grid cells.

Accessibility

Primary calendar views expose named grids, rows, cells, events, toolbar controls, resize separators, and modal dialogs to assistive technology. Cells use roving focus, so only one grid cell participates in the page Tab order. Use the arrow keys to move between cells, Home/End to move to the first or last cell in the current row or time column, and Tab to leave the grid.

Selection, event movement, resize results, validation failures, navigation, and cancellations are announced through a polite live region. Opening the more-events dialog traps focus inside it; Escape closes the dialog and restores focus to its trigger. Moving and resizing always have keyboard alternatives, so drag gestures are not required.

Range selection

Set selectable: true to enable pointer, touch, and keyboard selection in the month, day, week, resource, and resource time-grid views. Month and resource ranges are all-day; day, week, and resource time-grid ranges use the active time grid. Resource selections include the selected column's resourceId. Range ends are always exclusive.

const calendar = new WtsCalendar({
  container,
  selectable: true,
  selectMirror: true,
  selectMinDistance: 6,
  selectLongPressDelay: 700,
  unselectAuto: true,
  unselectCancel: '.event-editor',
  selectOverlap: event =>
    event.display === 'background' || event.display === 'inverse-background',
  selection: {
    snapDuration: 15,
    minimumDuration: 30,
  },
  selectAllow: selection => selection.end > selection.start,
  dateClick: info => {
    console.log(info.dateStr, info.allDay, info.resource);
  },
  select: selection => {
    console.log(
      selection.startStr,
      selection.endStr,
      selection.resource,
    );
  },
  unselect: ({ selection, jsEvent }) => {
    console.log('cleared', selection.startStr, jsEvent?.type);
  },
});

calendar.select({
  start: '2026-08-04T09:00:00',
  end: '2026-08-04T10:00:00',
});
calendar.getSelection();
calendar.unselect();

Press or drag across selectable cells to create a range. Keyboard users can focus a cell, press Enter or Space to start, extend with the arrow keys, and press Enter or Space to commit. Escape cancels a draft or clears the committed range. Accepted ranges emit select; clearing or replacing a range emits unselect; rejected ranges emit select-invalid with the constraint result. Selections reuse interactionConstraints for business hours, overlap, and resource capacity. selectConstraint: 'businessHours', an event groupId, or a dedicated constraint object limits only selections. The root selectOverlap option matches FullCalendar semantics: false rejects every intersecting foreground or background event, while a function approves each intersecting event. selection.allowOverlap remains supported as a legacy foreground-only alias.

dateClick fires for an unambiguous date/time activation. A positive selectMinDistance separates a mouse click from a drag selection. Touch input uses selectLongPressDelay: movement before the hold remains available for page/calendar scrolling, and movement after the hold extends the selection. selectMirror repla