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

@the-curve-consulting/react-scheduler

v0.5.1

Published

<div align="center"> <img src="logo.svg" style="height: 60px" alt="@the-curve-consulting/react-scheduler"> <hr /> <p align="center"> Open sourced, typescript oriented, light-weight, and ultra fast React Component for creating gantt charts. </p

Readme

Installation

# yarn
yarn add '@the-curve-consulting/react-scheduler'
# npm
npm install '@the-curve-consulting/react-scheduler'

This package expects react, react-dom, and styled-components from the consuming app.

Releasing to GitHub Packages

This repository is configured to publish @the-curve-consulting/react-scheduler to GitHub Packages when you publish a GitHub release.

  1. Develop changes on a feature branch and merge them into master through a pull request.

  2. Check out master locally and make sure it is up to date.

  3. Update the package version. This creates both a commit and a Git tag:

npm version patch

Use patch, minor, or major depending on the change.

To cut a prerelease instead, use a version with a prerelease suffix — for a new minor line, npm version preminor --preid alpha gives 0.4.0-alpha.0, and npm version prerelease bumps it to 0.4.0-alpha.1 and so on.

The workflow reads the version and picks the npm dist-tag from it: anything with a - in it publishes under next, everything else under latest. That matters because npm does not work this out for itself — without an explicit --tag it moves latest to whatever was published last, which would make a prerelease the default install for anyone running npm install without a version. Consumers of a prerelease should ask for it by name:

npm install @the-curve-consulting/react-scheduler@next
# or pin it
npm install @the-curve-consulting/[email protected]
  1. Push master and the new version tag:
git push origin master --follow-tags
  1. In GitHub, open the repository Releases page and create a new release from the tag that npm version created, for example v0.3.1.

  2. Publishing that release triggers .github/workflows/publish-package.yml, which builds the library and publishes it to GitHub Packages using the repository GITHUB_TOKEN.

  3. After the workflow finishes, verify the package under the repository or org Packages page in GitHub.

Using in another repository

  1. Create a personal access token (classic) with at least read:packages.

  2. In the consuming repository, add an .npmrc file with:

@the-curve-consulting:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
  1. Set GITHUB_PACKAGES_TOKEN in your shell, local environment, or CI secrets.

  2. Install the package:

npm install @the-curve-consulting/react-scheduler
  1. Import it in the app:
import "@the-curve-consulting/react-scheduler/dist/style.css";
import { Scheduler } from "@the-curve-consulting/react-scheduler";

GITHUB_PACKAGES_TOKEN should be a personal access token (classic) with at least read:packages. If the package is private, the token also needs access to the owning org or repository.

Example usage

  1. import required styles for scheduler
import "@the-curve-consulting/react-scheduler/dist/style.css";
  1. Import Scheduler component into your project
import { useCallback, useMemo, useRef, useState } from "react";
import {
  FetchDataParams,
  ProjectDeleteUpdate,
  ProjectUpdate,
  Scheduler,
  SchedulerData,
  SchedulerHandle,
  HolidayTileClickData,
  SchedulerProjectData,
  SchedulerProps
} from "@the-curve-consulting/react-scheduler";

type PlanningMeta = {
  scenarioId: string;
  externalId: string;
};

export default function Component() {
  const schedulerRef = useRef<SchedulerHandle<PlanningMeta>>(null);
  const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
  const scenarioId = "scenario-1";

  const handleFetchData = useCallback(
    async ({ range, signal }: FetchDataParams): Promise<SchedulerData<PlanningMeta>> => {
      const response = await fetch(
        `/api/planning?startDate=${range.startDate.toISOString()}&endDate=${range.endDate.toISOString()}&scenarioId=${scenarioId}`,
        { signal }
      );

      if (!response.ok) {
        throw new Error(`Failed to load planning data: ${response.status}`);
      }

      return response.json();
    },
    [scenarioId]
  );

  const transformData = useCallback(
    (rows: SchedulerData<PlanningMeta>) =>
      selectedUserIds.length ? rows.filter((row) => selectedUserIds.includes(row.id)) : rows,
    [selectedUserIds]
  );

  const handleTileClick = useCallback((project: SchedulerProjectData<PlanningMeta>) => {
    console.log(project.meta?.externalId);
  }, []);

  const handleHolidayClick: NonNullable<SchedulerProps<PlanningMeta>["onHolidayClick"]> =
    useCallback((holidayClick: HolidayTileClickData) => {
      console.log(
        holidayClick.resourceId,
        holidayClick.startDate,
        holidayClick.endDate,
        holidayClick.holidayRequests.map((request) => request.id)
      );
    }, []);

  const optimisticProject: ProjectUpdate<PlanningMeta> = useMemo(
    () => ({
      rowId: "user-1",
      projects: [
        {
          id: "project-1",
          title: "Project A",
          startDate: new Date("2026-03-01"),
          endDate: new Date("2026-03-07"),
          occupancy: 3600,
          meta: {
            scenarioId,
            externalId: "resource-block-1"
          }
        }
      ]
    }),
    [scenarioId]
  );

  const deletion: ProjectDeleteUpdate = {
    rowId: "user-1",
    projectIds: ["project-1"]
  };

  return (
    <section>
      <Scheduler<PlanningMeta>
        ref={schedulerRef}
        dataSourceKey={scenarioId}
        onFetchData={handleFetchData}
        transformData={transformData}
        onRangeChange={(range) => console.log("visible range", range)}
        onTileClick={handleTileClick}
        onHolidayClick={handleHolidayClick}
        onItemClick={(item) => console.log(item)}
        onEmptyClick={({ resourceId, startDate, endDate }) =>
          console.log("add work", resourceId, startDate, endDate)
        }
        onTileChange={(change) => console.log(change.reason, change.days, change.groupId)}
        onFilterData={() => setSelectedUserIds(["user-1", "user-2"])}
        onClearFilterData={() => setSelectedUserIds([])}
        config={{
          zoom: 0,
          filterButtonState: selectedUserIds.length ? 1 : 0
        }}
      />

      <button onClick={() => schedulerRef.current?.upsertProjects([optimisticProject])}>
        Optimistically add project
      </button>
      <button onClick={() => schedulerRef.current?.deleteProjects([deletion])}>
        Remove project
      </button>
      <button onClick={() => schedulerRef.current?.invalidate()}>Refetch current range</button>
    </section>
  );
}
  1. If some problems occur, please see our troubleshooting section below.

<Gantt>

<Gantt> is a second top-level component for project plans: a tree of tasks against a timeline, with dependency arrows, milestones, progress and baselines.

It is a sibling of <Scheduler>, not a mode of it. They share the calendar provider, the grid and header canvas painters, zoom, theming and i18n, but their data means different things — <Scheduler> answers "how much of this person's day is taken?", so every tile it draws carries an occupancy or a throughput; a Gantt task answers "when does this work happen, and what precedes it?" and has no such quantity. Keeping them separate means neither carries the other's fields and changes to one cannot reach the other.

import { Gantt } from "@the-curve-consulting/react-scheduler";
import type { GanttData, GanttTaskChange } from "@the-curve-consulting/react-scheduler";

const data: GanttData = {
  tasks: [
    { id: "phase-1", title: "Discovery", kind: "summary", startDate: a, endDate: b },
    {
      id: "audit",
      parentId: "phase-1",
      title: "Content audit",
      startDate: a,
      endDate: b,
      progress: 100
    },
    { id: "gate", title: "Signed off", kind: "milestone", startDate: c, endDate: c }
  ],
  links: [{ id: "l1", predecessorId: "audit", successorId: "gate" }]
};

<Gantt
  data={data}
  config={{ zoom: 1, outlineWidth: 240 }}
  onTaskChange={(change: GanttTaskChange) => save(change)}
/>;

Props

| Property | Type | Description | | ---------------- | ------------------ | ------------------------------------------------------------------- | | data | GanttData | { tasks, links } | | centerDate | string \| Date | date to centre the timeline on at mount | | config | GanttConfig | Config plus the Gantt options below | | isLoading | boolean | blocks interaction and shows the loading treatment | | defaultCollapsed | string[] | task ids that start collapsed | | onRangeChange | function | fires on scroll with the dates now on screen | | onTaskClick | function | a bar, milestone or outline row was clicked | | onTaskChange | function | a bar was dragged or resized — see below | | outlineLabel | string | heading above the outline column | | emptyMessage | string | shown when there are no tasks | | toolbar | SchedulerToolbar | controls of the application in the toolbar, see Toolbar |

Gantt config

| Property | Type | Default | Description | | ------------- | --------- | ------- | ------------------------------------------------------------------------------------------- | | outlineWidth | number | 240 | width of the task tree beside the chart; 0 hides it so a host can render its own grid | | editable | boolean | true | drag to move and resize bars | | showLinks | boolean | true | draw dependency arrows | | showBaselines | boolean | true | draw a ghost bar under any task carrying a baseline |

Everything in Config (zoom, lang, translations, theme, defaultTheme, showThemeToggle) applies as well.

Tasks

kind is "task" (default), "milestone" (zero duration, drawn as a diamond) or "summary" (drawn as a bracket and never draggable — its span belongs to its children). Nesting is by parentId; parents may appear after their children, and sibling order is the order tasks arrive in. A parentId that names no supplied task is treated as a root rather than stranding the subtree, and a cycle in parentId is broken rather than recursing.

Optional per task: progress (0-100, filled inside the bar), bgColor, subtitle, assignees (drawn as initials beside the bar), baseline ({ startDate, endDate }, drawn as a ghost bar), locked (suppresses drag), and meta for your own typed payload.

Links

{ id, predecessorId, successorId, kind } where kind is finish_to_start (default), start_to_start, finish_to_finish or start_to_finish. Arrows route orthogonally, and drop into the gutter between rows when the successor starts before the predecessor ends. A link whose end is hidden under a collapsed parent is not drawn.

Editing

onTaskChange reports { id, startDate, endDate, reason }, where reason is "move", "resize-start" or "resize-end". The component does not move the bar itself. Apply the change to your own data and pass it back, so that any rescheduling the plan implies — pushing successors, rolling summaries up — happens in one place rather than being split between your code and this component's. Drags snap to whole days.

Scheduler API

Scheduler Component Props

| Property Name | Type | Arguments | Description | | ----------------- | ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | data | SchedulerData | - | scheduler rows to display in static mode | | initialData | SchedulerData | - | optional initial cache seed for async mode when using onFetchData | | isLoading | boolean | - | external loading flag; forces blocking loading state | | startDate | string | ISO date string | initial date to center scheduler on mount | | dataSourceKey | string | - | async cache identity; changing it invalidates cached prefetched data | | onRangeChange | function | updated startDate and endDate | callback fired when visible date range changes (called every scroll event) | | onFetchData | function | range, direction, reason, signal | async data source used for initial fetch, edge prefetch and hard jumps (called when insufficient cached data) | | onTileClick | function | clicked resource data | detects resource click | | onHolidayClick | function | clicked holiday range data | detects holiday tile click and returns the clicked row id, clicked range, and matching holiday requests | | onItemClick | function | clicked left column item data | detects item click on left column | | onEmptyClick | function | resourceId, startDate, endDate | detects a click or a drag over days of a resource row that hold no tile; the grid marks the days under the pointer while this is set | | onTileChange | function | SchedulerTileChange, clicked tile data | detects a drag of a tile to a new place or a new length; the grid previews the drag while this is set | | onFilterData | function | - | callback firing when filter button was clicked | | onClearFilterData | function | - | callback firing when clear filters button was clicked (clearing button is visible only when filterButtonState is set to >0) | | transformData | function | SchedulerData | transforms cached scheduler data before rendering, useful for local filtering | | config | Config | - | object with scheduler config properties | | toolbar | SchedulerToolbar | - | controls of the application in the top bar, see Toolbar |

Scheduler supports two exclusive data modes:

  • static mode: pass data
  • async mode: pass onFetchData and optionally initialData
onFetchData callback contract

Return type: Promise<SchedulerData<TMeta>>

| Field | Type | Description | | ----------- | ----------------------------------- | --------------------------------------------------------------------------- | | range | { startDate: Date; endDate: Date} | date window requested by scheduler | | direction | "backward" \| "forward" | side from which scheduler requests additional data | | reason | "initial" \| "prefetch" \| "jump" | why request was created | | signal | AbortSignal \| undefined | abort signal for cancelling stale requests (recommended to handle in fetch) |

Imperative API

Use ref when you want to mutate cached data without invalidating the whole scheduler.

import {
  ProjectDeleteUpdate,
  ProjectUpdate,
  SchedulerHandle
} from "@the-curve-consulting/react-scheduler";

type PlanningMeta = {
  externalId: string;
};

const schedulerRef = useRef<SchedulerHandle<PlanningMeta>>(null);

const optimisticProject: ProjectUpdate<PlanningMeta> = {
  rowId: "user-1",
  projects: [
    {
      ...project,
      meta: {
        externalId: "resource-block-1"
      }
    }
  ]
};

schedulerRef.current?.upsertProjects([optimisticProject]);

const deletion: ProjectDeleteUpdate = {
  rowId: "user-1",
  projectIds: [project.id]
};

schedulerRef.current?.deleteProjects([deletion]);
schedulerRef.current?.invalidate();

Available handle methods:

| Method | Arguments | Description | | ---------------- | ----------------------- | -------------------------------------------------------------------------- | | invalidate | - | clears async cache for the current data source and refetches visible range | | upsertProjects | ProjectUpdate[] | creates or updates projects inside existing rows | | deleteProjects | ProjectDeleteUpdate[] | removes projects by id from existing rows |

Custom project metadata

SchedulerProjectData is generic, so you can attach your own project payload with meta and keep it typed in onFetchData, onTileClick, transformData, and the imperative handle API.

type TimesheetMeta = {
  entryId: string;
  userId: string;
};

const project: SchedulerProjectData<TimesheetMeta> = {
  id: "entry-1",
  title: "Timesheet",
  startDate: new Date("2026-03-01"),
  endDate: new Date("2026-03-01"),
  occupancy: 3600,
  meta: {
    entryId: "ts-123",
    userId: "user-1"
  }
};
Scheduler Config Object

| Property Name | Type | Default | Description | | ----------------- | ---------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | zoom | 0 or 1 or 2 | 0 | 0 - display grid divided into weeks 1 - display grid divided into days 2 - display grid divided into hours | | filterButtonState | number | 0 | < 0 - hides filter button, 0 - state for when filters were not set, > 0 - state for when some filters were set (allows to also handle onClearFilterData event) | | maxRecordsPerPage | number | 50 | number of rows (projects) from SchedulerData visible per page | | lang | en, lt or pl | en | scheduler's language | | showTooltip | boolean | true | show tooltip when hovering over tiles | | translations | LocaleType[] | undefined | option to add specific langs translations | | showThemeToggle | boolean | false | show toggle button to switch between light/dark mode | | defaultTheme | light or dark | light | scheduler's default theme | | theme | Theme | undefined | custom light/dark theme color overrides | | maxHoursPerWeek | number | 40 | fallback maximum week capacity used to derive default Monday-Friday working durations and default holiday half-day duration | | defaultStartHour | number | 9 | start hour used when placing entries and partial-day holidays in hourly view | | dataLoading | DataLoadingConfig | built-in defaults | controls prefetching and cache window used by onFetchData flow | | headerFonts | Partial<HeaderFonts> | built-in fonts | canvas fonts of the header rows, for example "600 13px Inter"; see HeaderFonts | | headerUppercase | boolean | true | writes the header labels in capitals ("MON", "SEPTEMBER"); false gives "Mon" and "September" | | tileStyle | solid or tinted | solid | solid fills a tile with its colour; tinted gives a pale tint with a bar of the colour and dark text, a hatched non-working day with no label, and only the title on a tile narrower than 250px |

Toolbar

toolbar puts controls of your application in the top bar of <Scheduler> or <Gantt>, so a page does not need a second row of controls. start goes before the navigation and end goes before the zoom controls. The component keeps its own navigation and zoom.

<Scheduler
  config={config}
  toolbar={{
    start: <PeopleFilter />,
    end: <ScenarioSelect />
  }}
/>

The toolbar is inside the component, so a popover that opens to the left of a start control can go under the left column. Open it to the right.

Pass a config object that keeps its identity between renders, for example a constant or a useMemo. The theme and the calendar context are built from it, and a new object on every render draws the header again and renders every tile again.

HeaderFonts

| Property Name | Type | Default | Description | | --------------- | -------- | ------------------ | ---------------------------------------------------- | | topRow | string | "600 14px Inter" | the month in the day view, the year in the week view | | middleRow | string | "400 10px Inter" | the week in the day view, the month in the week view | | bottomRowName | string | "600 14px Inter" | the name of the day or the week | | bottomRowNumber | string | "600 10px Inter" | the date, and the day and hour rows of the hour view |

DataLoadingConfig

| Property Name | Type | Default | Description | | -------------------- | -------- | ------- | ------------------------------------------------------------- | | initialLoadDays | number | 90 | initial range size (days) requested around first visible date | | prefetchDays | number | 60 | number of days requested for each prefetch | | prefetchTriggerDays | number | 45 | day-based threshold for triggering edge prefetch | | prefetchTriggerRatio | number | 0.7 | ratio-based threshold for triggering edge prefetch | | maxCachedDays | number | 120 | soft cache half-window retained around current view | | requestDebounceMs | number | 80 | debounce for rapid prefetch triggers | | jumpWindowMultiplier | number | 3 | multiplier for hard-jump fetch window vs visible range | | minJumpWindowDays | number | 45 | minimum hard-jump fetch window |

Translation object example

import enDayjsTranslations from "dayjs/locale/en";

const langs: LocaleType[] = [
  {
    id: "en",
    lang: {
      feelingEmpty: "I feel so empty...",
      free: "Free",
      loadNext: "Next",
      loadPrevious: "Previous",
      over: "over",
      taken: "Taken",
      topbar: {
        filters: "Filters",
        next: "next",
        prev: "prev",
        today: "Today",
        view: "View"
      },
      search: "search",
      week: "week"
    },
    translateCode: "en-EN",
    dayjsTranslations: enDayjsTranslations
  }
];

<Scheduler
  // ... //
  config={{
    lang: "en",
    translations: langs
  }}
/>;

Scheduler LocaleType Object

| Property Name | Type | Description | | ----------------- | -------------------------- | ---------------------------------- | | id | string | key is needed for selecting lang | | lang | Translation | object with translations | | translateCode | string | code that is saved in localStorage | | dayjsTranslations | string ILocale undefined | object with translation from dayjs |

Scheduler Translation Object

| Property Name | Type | | ------------- | -------- | | feelingEmpty | string | | free | string | | loadNext | string | | loadPrevious | string | | over | string | | taken | string | | search | string | | week | string | | topbar | Topbar |

Scheduler Topbar Object

| Property Name | Type | | ------------- | -------- | | filters | string | | next | string | | prev | string | | today | string | | view | string |

Scheduler Data

array of chart rows with shape of | Property Name | Type | Description | | -------- | --------------------- | -------------------------------- | | id | string | unique row id | | label | SchedulerRowLabel | row's label, e.g person's name, surname, icon | | data | Array<ResourceItem> | array of resources | | holidayRequests | HolidayRequest[] | row-level holiday requests used for holiday tiles and holiday-adjusted occupancy | | workingDurations | WorkingDuration[] (optional) | row-level working hours used for occupancy, throughput, tooltips, and non-working tile segments |

HolidayRequest

Rows must provide holidayRequests. Use an empty array when the row has no holidays.

Holiday requests render as background holiday tiles. When a holiday tile is clicked, onHolidayClick receives HolidayTileClickData with the clicked row id, clicked date range, and all row-level holiday requests overlapping that range. Weekly zoom returns the clicked ISO week. Daily and hourly zoom return the clicked day.

Holiday requests also reduce available working time used by tooltips, throughput calculations, and non-working tile segmentation. Full-day and multi-day holidays remove the affected working time. Partial-day holidays remove half of the default workday derived from config.maxHoursPerWeek, not half of the row's custom working hours. In hourly zoom, the half-day is placed from config.defaultStartHour; in daily and weekly zoom, partial holidays render as half of the visual day cell.

Scheduler does not filter holiday requests by state. Pass only the requests that should be rendered and included in capacity calculations.

For partial holidays, Morning removes the first half of the day. Other defined morning_or_afternoon values are treated as the second half of the day.

| Property Name | Type | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | id | string | unique holiday request id | | leave_from | Date | first date covered by the holiday request | | leave_to | Date | last date covered by the holiday request | | leave_type | "Sick Leave" \| "Holiday / Vacation" \| "Compassionate Leave" \| "Unpaid Leave" \| "Paternity Leave" \| "Unknown" | holiday category returned by the data source | | state | "pending" \| "approved" \| "rejected" \| "cancelled" | request state returned by the data source | | morning_or_afternoon | "Morning" \| "Afternoon" \| "Half Day" (optional) | partial-day marker; omitted values are treated as full-day holiday blocks |

HolidayTileClickData

onHolidayClick receives this payload when a holiday tile is clicked.

| Property Name | Type | Description | | --------------- | ------------------ | ------------------------------------------------------------------ | | resourceId | string | row id for the resource whose holiday tile was clicked | | startDate | Date | first date/time covered by the clicked holiday range | | endDate | Date | last date/time covered by the clicked holiday range | | holidayRequests | HolidayRequest[] | holiday requests from the clicked row that overlap the click range |

WorkingDuration

Rows can define workingDurations when a person has custom working hours. If omitted, Scheduler defaults to Monday-Friday working days derived from config.maxHoursPerWeek (40 hours by default).

When multiple working durations are provided, Scheduler uses the latest effectiveFrom date that is not after the rendered date. This allows working hours to change over time.

| Property Name | Type | Description | | ------------- | -------------- | --------------------------------------------------- | | effectiveFrom | Date | first date from which this working duration applies | | flexibleHours | boolean | flag preserved on the row model | | workingDays | WorkingDay[] | weekdays and hours worked on each day |

WorkingDay

| Property Name | Type | Description | | ------------- | ------------------------------------------------------------------------------------------ | -------------------------- | | day | "Monday" \| "Tuesday" \| "Wednesday" \| "Thursday" \| "Friday" \| "Saturday" \| "Sunday" | day of week | | hours | number | working hours for that day |

Example row with custom working hours and mixed project duration models:

const data: SchedulerData<PlanningMeta> = [
  {
    id: "user-1",
    label: {
      icon: "JD",
      title: "Jane Doe",
      subtitle: "Consultant"
    },
    holidayRequests: [
      {
        id: "holiday-1",
        leave_from: new Date("2026-03-04"),
        leave_to: new Date("2026-03-04"),
        leave_type: "Holiday / Vacation",
        state: "approved",
        morning_or_afternoon: "Morning"
      }
    ],
    workingDurations: [
      {
        effectiveFrom: new Date("2026-01-01"),
        flexibleHours: false,
        workingDays: [
          { day: "Monday", hours: 7.5 },
          { day: "Tuesday", hours: 7.5 },
          { day: "Wednesday", hours: 7.5 },
          { day: "Thursday", hours: 7.5 },
          { day: "Friday", hours: 6 }
        ]
      }
    ],
    data: [
      {
        id: "fixed-time-project",
        title: "Fixed time project",
        startDate: new Date("2026-03-02"),
        endDate: new Date("2026-03-06"),
        occupancy: 3600
      },
      {
        id: "throughput-project",
        title: "Throughput project",
        startDate: new Date("2026-03-02"),
        endDate: new Date("2026-03-06"),
        throughput: 0.8
      }
    ]
  }
];
Left Colum Item Data

data that is accessible as argument of onItemClick callback | Property Name | Type | Description | | -------- | --------------------- | -------------------------------- | | id | string | unique row id | | label | SchedulerRowLabel | row's label, e.g person's name, surname, icon |

Resource Item

item that will be visible on the grid as tile and that will be accessible as argument of onTileClick event | Property Name | Type | Description | | ----------- | ----------------- | ------------------------------------------------------------------------------------------------------- | | id | string | unique resource id | | title | string | resource title that will be displayed on resource tile | | subtitle | string (optional) | resource subtitle that will be displayed on resource tile | | description | string (optional) | resource description that will be displayed on resource tile | | startDate | Date | date for calculating start position for resource | | endDate | Date | date for calculating end position for resource | | occupancy | number | fixed number of seconds the resource takes per working day | | throughput | number | fraction of the person's holiday-adjusted working day that the resource takes, e.g. 0.8 means 80% of available working hours | | bgColor | string (optional) | tile color | | meta | TMeta (optional) | custom project payload preserved by scheduler and exposed in typed callbacks |

Provide either occupancy or throughput for a resource item.

Throughput is calculated per available working day. Multiple throughput resources whose total is greater than 1 can create overtime; the scheduler does not cap the requested work at 100% before calculating tooltip overtime.

Troubleshooting

  • For using Scheduler with RemixJS make sure to add @the-curve-consulting/react-scheduler to serverDependenciesToBundle in remix.config.js like so:
// remix.config.js
/** @type  {import('@remix-run/dev').AppConfig} */
module.exports = {
	// ...
	serverDependenciesToBundle: [..., "@the-curve-consulting/react-scheduler"],
};
  • When using with NextJS (app router) Scheduler needs to be wrapped with component with use client
"use client"
import { Scheduler, SchedulerProps } from "@the-curve-consulting/react-scheduler";

default export function SchedulerClient(props: SchedulerProps) {
	return <Scheduler {...props} />;
}
  • When using with NextJS (pages router) it needs to be imported using dynamic:
import dynamic from "next/dynamic";
const Scheduler = dynamic(
  () => import("@the-curve-consulting/react-scheduler").then((mod) => mod.Scheduler),
  {
    ssr: false
  }
);
  • How to customize Scheduler dimensions

Scheduler is position absolutely to take all available space. If you want to have fixed dimensions wrap Scheduler inside a div with position set to relative.

Example using styled components:

export const StyledSchedulerFrame = styled.div`
  position: relative;
  height: 40vh;
  width: 40vw;
`;

<StyledSchedulerFrame>
    <Scheduler  {...}/>
</StyledSchedulerFrame>

Known Issues

  1. No responsiveness
  2. Slower performance on Firefox when working with big set of data due to Firefox being slower working with canvas

How to contribute

  • Reporting Issues: If you come across any bugs, glitches, or have any suggestions for improvements, please open an issue on our GitHub repository. Provide as much detail as possible, including steps to reproduce the issue.
  • Suggesting Enhancements: If you have ideas for new features or enhancements, we would love to hear them! You can open an issue on our GitHub repository and clearly describe your suggestion.
  • Submitting Pull Requests: If you have developed a fix or a new feature that you would like to contribute, you can submit a pull request. Here's a quick overview of the process:
    • Clone the repository and create your own branch: git checkout -b feat/your-branch-name.
    • Implement your changes, following the code style and guidelines from development.md.
    • Test your changes to ensure they work as expected.
    • Commit your changes and push to your forked repository.
    • Open a pull request against our main repository's master branch.
      • add at least 1 reviewer
      • link correct issue

License

MIT Licensed. Copyright (c) Bitnoise 2023, The Curve Consulting Services Limited 2026.