npm package: @innosolutions/inno-calendar
Description: A headless-first, fully customizable React calendar
Author: InnoSolutions
Latest version: 1.0.60

# @inno/calendar

A headless-first, fully customizable React calendar for enterprise applications.

[![npm version](https://img.shields.io/npm/v/@innosolutions/inno-calendar.svg)](https://www.npmjs.com/package/@innosolutions/inno-calendar)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18+-61DAFB.svg)](https://reactjs.org/)

> **🤖 AI Agents:** For comprehensive implementation details, copy the contents of [AGENT.md](./AGENT.md) into your context.

## Installation

```bash
npm install @innosolutions/inno-calendar
```

```bash
# Optional: Radix UI for enhanced popovers/tooltips
npm install @radix-ui/react-popover @radix-ui/react-tooltip @radix-ui/react-dropdown-menu
```

## Quick Start

```tsx
import { InnoCalendar, type CalendarEvent } from "@innosolutions/inno-calendar";
import "@innosolutions/inno-calendar/styles";

const events: CalendarEvent[] = [
  {
    id: "1",
    title: "Team Meeting",
    startDate: new Date("2026-02-04T09:00:00"),
    endDate: new Date("2026-02-04T10:00:00"),
    color: "blue",
  },
];

function MyCalendar() {
  return (
    <InnoCalendar
      events={events}
      initialView="week"
      onEventClick={(event) => console.log(event)}
      onSlotSelect={(selection) =>
        console.log(selection.startDate, selection.endDate)
      }
    />
  );
}
```

## Features

- **8 View Modes** — Day, Week, Month, Year, Agenda, Timeline (1/3/7 day)
- **Headless Architecture** — Use hooks directly or pre-built components
- **Drag & Drop** — Drag to select time ranges, drag events to reschedule
- **TypeScript** — Fully typed with generic event data support
- **Customizable** — Custom popovers, settings panels, filters
- **No External Dependencies** — Native Date API, minimal footprint

## Views

| View            | Description                |
| --------------- | -------------------------- |
| `day`           | Single day hourly grid     |
| `week`          | 7-day grid with time slots |
| `month`         | Monthly calendar           |
| `year`          | 12-month overview          |
| `agenda`        | Scrollable event list      |
| `timeline-day`  | Resource timeline (1 day)  |
| `timeline-3day` | Resource timeline (3 days) |
| `timeline-week` | Resource timeline (7 days) |

## Event Structure

```tsx
interface CalendarEvent<TData = Record<string, unknown>> {
  // Required
  id: string;
  title: ReactNode; // String or JSX
  startDate: Date;
  endDate: Date;

  // Optional display fields
  color?:
    | "blue"
    | "green"
    | "red"
    | "yellow"
    | "purple"
    | "orange"
    | "pink"
    | "teal"
    | "gray"
    | "indigo";
  hexColor?: string; // Hex override (takes precedence over color)
  description?: ReactNode; // String or JSX
  isCanceled?: boolean;
  isAllDay?: boolean;
  isMultiDay?: boolean;
  isRecurring?: boolean;
  resourceId?: string; // For resource/timeline views

  // Built-in filtering (optional — skip if you filter server-side)
  scheduleTypeId?: number;
  scheduleTypeName?: ReactNode; // String or JSX
  participants?: ICalendarUser[];

  // @deprecated — still functional, but migrate to `data` bag.
  // Will be removed in the next major version.
  meetingTookPlace?: boolean; // → data.meetingTookPlace
  isAccepted?: boolean; // → data.isAccepted
  companyId?: number; // → data.companyId
  cancelReason?: string | null; // → data.cancelReason
  user?: ICalendarUser; // → data.user
  // ... and others (see types.ts for full list)

  data?: TData; // Your custom domain-specific fields (RECOMMENDED)
}
```

### ReactNode Props

`title`, `description`, and `scheduleTypeName` accept `ReactNode` — you can pass plain strings (fully backwards compatible) or custom JSX:

```tsx
const event: CalendarEvent = {
  id: "1",
  title: <span><UserIcon className="h-3 w-3 inline" /> John Doe</span>,
  description: <p className="italic">VIP client meeting</p>,
  startDate: new Date(),
  endDate: new Date(),
};
```

The built-in search and `aria-label` automatically extract plain text from JSX nodes via the `reactNodeToText` utility.

### Extended Props (data.extendedProps)

For rendering an arbitrary number of custom lines on EventCard/EventBlock, use the `extendedProps` array in the `data` bag:

```tsx
const event: CalendarEvent = {
  id: "1",
  title: "Practical Course",
  startDate: new Date(),
  endDate: new Date(),
  data: {
    extendedProps: [
      <p key="student" className="font-medium">Student: Jane Doe</p>,
      <p key="phone">Phone: +32 123 456 789</p>,
      <div key="info" className="flex items-center gap-1">
        <CarIcon className="h-3 w-3" />
        <span>Automatic transmission</span>
      </div>,
    ],
  },
};
```

Each entry renders as its own row. Visible on EventCard (full variant) and EventBlock (events >= 45min). Also shown in tooltips.

## Props

```tsx
<InnoCalendar
  // Data
  events={events}
  users={users}
  scheduleTypes={scheduleTypes}
  // Initial State
  initialView="week"
  initialDate={new Date()}
  // Callbacks
  onEventClick={(event) => {}}
  onSlotSelect={(selection) => {}}
  onSlotClick={(date, hour) => {}}
  onAddEvent={() => {}}
  onEventDrop={(result) => {}}
  onDateChange={(date, view) => {}}
  onViewChange={(view) => {}}
  // Customization
  renderPopover={({ event, onClose }) => <CustomPopover />}
  settingsContent={<SettingsPanel />}
  filterContent={<FiltersRow />}
  showHeader={true}
/>
```

## Provider Pattern

For shared state across components:

```tsx
import {
  InnoCalendarProvider,
  useInnoCalendar,
  CalendarHeader,
  WeekView,
} from "@innosolutions/inno-calendar";

function App() {
  return (
    <InnoCalendarProvider initialEvents={events} initialView="week">
      <CalendarContent />
    </InnoCalendarProvider>
  );
}

function CalendarContent() {
  const { view, filteredEvents, selectedDate } = useInnoCalendar();
  return (
    <>
      <CalendarHeader />
      <WeekView events={filteredEvents} date={selectedDate} />
    </>
  );
}
```

## Working Hours

```tsx
const workingHours = {
  0: { enabled: false, from: 8, to: 17 }, // Sunday
  1: { enabled: true, from: 9, to: 18 }, // Monday
  2: { enabled: true, from: 9, to: 18 }, // Tuesday
  3: { enabled: true, from: 9, to: 18 }, // Wednesday
  4: { enabled: true, from: 9, to: 18 }, // Thursday
  5: { enabled: true, from: 9, to: 17 }, // Friday
  6: { enabled: false, from: 8, to: 12 }, // Saturday
};

<InnoCalendar
  events={events}
  preferencesConfig={{
    defaults: { workingHours },
  }}
/>;
```

## Styles

Import the bundled styles for proper rendering:

```tsx
import "@innosolutions/inno-calendar/styles";
```

Or include CSS custom properties for theming:

```css
:root {
  --inno-border-color: #e5e7eb;
  --inno-primary: #3b82f6;
  --inno-hour-height: 96px;
}
```

## TypeScript

```tsx
import type {
  CalendarEvent,
  TCalendarView,
  TEventColor,
  ICalendarUser,
  IScheduleType,
  ISelectionResult,
  TWorkingHours,
} from "@innosolutions/inno-calendar";

// Utility: extract plain text from any ReactNode (for search, labels, etc.)
import { reactNodeToText } from "@innosolutions/inno-calendar";
```

## License

MIT © [InnoSolutions](https://github.com/innosolutions)