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

@innosolutions/inno-calendar

v2.0.2

Published

A headless-first, fully customizable React calendar

Readme

@innosolutions/inno-calendar

A React calendar that behaves like Google Calendar and lets your application own everything domain-specific.

npm version TypeScript React

Upgrading from 1.x? See CHANGELOG.md — 2.0.0 removes slots, classNames and the /presets entry point.

What it owns, and what it doesn't

The package owns the parts that are the same in every calendar: the grid, the header, the floating card a drag opens, the detail popover, the filter rail, the drag-and-drop, the placement maths.

Your application owns the parts that differ: what an event is, what a form asks for, what a detail card shows, what you can filter by. You pass those in as components. There is one extension mechanism — render props — not three.

<InnoCalendar
  events={events}
  eventCard={(event, { compact }) => <MyCard event={event} compact={compact} />}
  eventDetail={(event, { close, edit }) => <MyDetail … />}
  eventForm={({ selection, event }, { close, expand }) => <MyForm … />}
  filters={({ close }) => <MyFilters onDone={close} />}
/>

Installation

npm install @innosolutions/inno-calendar

react and react-dom (18 or 19) are the only peer dependencies. Everything else the calendar needs — Radix popover, dropdown, tooltip and dialog, plus lucide-react — comes with it.

Quick start

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={({ startDate, endDate }) => console.log(startDate, endDate)}
    />
  );
}

That renders a working calendar. Everything below is opt-in.

Views

| View | Description | | --- | --- | | day | Single day, hourly grid | | 4day | Four days | | week | Seven days with time slots | | month | Monthly grid | | year | Twelve-month overview | | agenda | Scrollable list | | resource-day / resource-week | One column per resource | | timeline-day / timeline-3day / timeline-week | One row per resource |

The event

interface CalendarEvent<TData = Record<string, unknown>> {
  id: string;
  title: ReactNode;
  startDate: Date;
  endDate: Date;

  color?: "blue" | "green" | "red" | "yellow" | "purple"
        | "orange" | "pink" | "teal" | "gray" | "indigo";
  hexColor?: string;          // overrides `color`
  description?: ReactNode;
  isCanceled?: boolean;
  isAllDay?: boolean;
  isRecurring?: boolean;
  resourceId?: string;        // resource and timeline views

  // Client-side filtering. Skip it if you filter server-side.
  scheduleTypeId?: number;
  participants?: ICalendarUser[];

  data?: TData;               // your domain fields live here
}

Put your own fields on data. The calendar never reads it — it hands the whole event back to your eventCard, eventDetail and eventForm, which are the only things that know what a "property" or an "opportunity" is.

type MyData = { raw: AgendaRow };

<InnoCalendar<MyData>
  events={rows.map((row) => ({ …, data: { raw: row } }))}
  eventCard={(event) => <MyCard row={event.data.raw} />}
/>

A handful of domain-shaped fields (meetingTookPlace, companyId, user, …) still exist at the top level and are marked @deprecated. They will go in 3.0.

Extension points

Each is a function returning JSX. Pass one to take that surface over; leave it out and the calendar handles it.

eventCard — the block in the grid

eventCard={(event, { compact }) => (
  <MyCard event={event} compact={compact} />
)}

compact is true in the month view and other tight rows.

eventDetail — what a click opens

eventDetail={(event, { close, edit }) => (
  <MyDetail event={event} onClose={close} onEdit={edit} />
)}

edit is present only when an eventForm is there to open. The calendar supplies the container: a popover beside the event, or a bottom sheet on a narrow screen, capped to the room available and scrolling inside.

eventForm — create and edit

eventForm={({ selection, event }, { close, expand }) => (
  <MyForm
    start={selection.startDate}
    end={selection.endDate}
    event={event}          // present when editing
    onDone={close}
    onExpand={expand}      // ask for the wider card
  />
)}

The calendar owns the card — placement, dragging it by its handle, Escape, click-outside, the height cap, the phone sheet. You own what is inside it. Pair it with the exported WhenRow and RecurrenceField so your form's time row behaves like the built-in one.

A drag on the grid opens it on that range. So does the Add button, on the slot happening now. ref.current.openForm() does the same from anywhere.

filters — the rail

filters={({ close }) => <MyFilters onDone={close} />}

The calendar supplies the rail on wide screens and a sheet on narrow ones; close puts the sheet away after a filter that applies on tap.

Props

<InnoCalendar
  // Data
  events={events}
  users={users}                       // resource rows
  isLoading={isFetching}

  // Extension points
  eventCard={…}
  eventDetail={…}
  eventForm={…}
  filters={…}

  // Initial state
  initialView="week"
  initialDate={new Date()}
  locale="fr"

  // Callbacks
  onEventClick={(event) => {}}
  onSlotSelect={(selection) => {}}
  onSlotClick={(date, hour) => {}}
  onAddEvent={() => {}}
  onEventDrop={(result) => {}}
  onDateChange={(date, view) => {}}
  onViewChange={(view) => {}}

  // Chrome
  showHeader
  showSidebar
  createLabel="Add"
  labels={{ header: {…}, agenda: {…}, filters: "Filters" }}
  headerActions={<MyActions />}
  settingsContent={<MySettings />}
  sidebarContent={<MyRail />}

  // Behaviour
  minSelectionMinutes={30}
  preferencesConfig={{ defaults: {…} }}
  resourceColumnConfig={{…}}
  alwaysShowResources={resources}
/>

onEventDrop is worth a note: without it, a drag or resize opens the form on the new times rather than writing them, because a reschedule that mails the participants should be confirmed. Pass a handler to take that over and get the optimistic save with undo instead.

The ref

const calendar = useRef<ICalendarRefHandle>(null);

calendar.current?.openForm();                       // new, on the slot happening now
calendar.current?.openForm({ start, end });         // new, on a range
calendar.current?.openForm({ event });              // edit, on its own times
calendar.current?.scrollToToday();
calendar.current?.focusEvent(id);

Working hours and preferences

preferencesConfig={{
  defaults: {
    slotDuration: 30,
    visibleHours: { from: 7, to: 20 },
    showWorkingHoursOnly: true,
    workingHours: { 1: { from: 9, to: 17 }, /* 0 = Sunday */ },
    badgeVariant: "colored",
  },
}}

Preferences persist per user in localStorage; defaults is what a user who has never changed anything sees.

Styling

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

Theme and dimensions are CSS custom properties:

:root {
  --inno-border-color: #e5e7eb;
  --inno-background: #ffffff;
  --inno-foreground: #111827;
  --inno-muted: #f3f4f6;
  --inno-primary: #3b82f6;

  --inno-hour-height: 96px;
  --inno-header-height: 56px;
  --inno-sidebar-width: 200px;

  --inno-event-blue: #3b82f6;   /* …and green, red, yellow, purple,
                                   orange, pink, teal, gray, indigo */
}

Dark mode reads the same names under .dark.

Anything more specific than those is CSS against the package's class names, which is not yet a stable contract — see the note in the changelog.

Stacking

Anything you portal from inside a calendar surface has to sit above it, and guessing a number that works today is how that breaks later:

import { IC_Z_LAYER } from "@innosolutions/inno-calendar";

<SelectContent style={{ zIndex: IC_Z_LAYER }}>

IC_Z_CARD (1000) is the floating card, IC_Z_LAYER (1100) menus and pickers opened from one, IC_Z_MODAL (1200) a dialog on top of that.

TypeScript

import type {
  CalendarEvent,
  ICalendarRefHandle,
  ICalendarUser,
  IEventFormActions,
  IEventFormArgs,
  IRecurrence,
  ISelectionResult,
  TCalendarView,
  TEventColor,
  TWorkingHours,
} from "@innosolutions/inno-calendar";

Exported components worth knowing: WhenRow, RecurrenceField, FieldLayer and QuickCreate for building a form body, and formatClockRange / reactNodeToText for labels.

License

MIT © InnoSolutions