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

@chiliec/appointment-calendar

v0.5.0

Published

Month-grid calendar + daily timeline React components for appointment booking UIs

Readme

@chiliec/appointment-calendar

React month-grid calendar + daily timeline components for appointment booking UIs.

Install

pnpm add @chiliec/appointment-calendar
# or: npm install @chiliec/appointment-calendar

Peer deps: react@^19, react-dom@^19.

Quick start (styled layer)

Import the stylesheet once in your app entry:

import "@chiliec/appointment-calendar/styles.css";

Then use the components:

import { useState } from "react";
import {
  MonthCalendar,
  DayTimeline,
} from "@chiliec/appointment-calendar/styled";

const LABELS = {
  today: "Today",
  weekdays: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] as const,
};

export function MyScheduler() {
  const [date, setDate] = useState("2026-05-26");
  const appointments = [
    { id: "1", date: "2026-05-26", start_time: "10:00", duration: 60 },
  ];
  const dayCounts = { "2026-05-26": appointments.length };

  return (
    <>
      <MonthCalendar
        selected={date}
        onSelect={setDate}
        dayCounts={dayCounts}
        labels={LABELS}
      />
      <DayTimeline
        date={date}
        appointments={appointments}
        onSlotClick={(time) => console.log("clicked", time)}
        labels={LABELS}
      />
    </>
  );
}

Headless layer

For full UI control, use the hooks and utilities directly:

import {
  useMonthGrid,
  useDayTimeline,
  findConflicts,
  buildMonthGrid,
  timeToMinutes,
} from "@chiliec/appointment-calendar";

Theming

The styled layer uses CSS variables on .ac-root. Override any of them on a parent element to re-theme:

.my-app .ac-root {
  --ac-bg: #fffbef;
  --ac-fg: #5f471d;
  --ac-accent: #ffdea3;
  --ac-radius: 1rem;
}

Class names are prefixed .ac- — see src/styled/styles.css for the full list.

API

<MonthCalendar />

| Prop | Type | Required | Notes | |---|---|---|---| | selected | string (YYYY-MM-DD) | yes | Currently selected date | | onSelect | (date) => void | yes | | | dayCounts | Record<string, number> | no | Workload dots per day | | initialYear / initialMonth | number | no | Defaults to today; month is 0-indexed | | locale | string | no | BCP-47, default "en-US" | | onMonthChange | (year, month) => void | no | Fires after prev/next/today nav. Useful for refetching workload counts. Not fired on mount. | | labels | Labels | yes | See "Labels" below |

<DayTimeline<T> />

| Prop | Type | Required | Notes | |---|---|---|---| | date | string (YYYY-MM-DD) | yes | | | appointments | T[] (T extends Appointment) | yes | | | workStart / workEnd | number | no | Default 9 / 21 | | onSlotClick | (time: "HH:MM") => void | no | | | renderAppointment | (apt: T) => ReactNode | no | | | getDisplayDuration | (apt: T) => number | no | For cleanup buffer, etc. | | onReschedule | (id: string, newStartHHMM: "HH:MM") => void | no | Fired when a block is dragged to a new start time. Enables drag-to-reschedule. | | canDrag | (apt: T) => boolean | no | Whether a block may be dragged. Default: all draggable (drag is only active when onReschedule is also set). | | snapMinutes | number | no | Snap increment while dragging, in minutes. Default 15. | | labels | Labels | yes | |

Drag-to-reschedule: when onReschedule is provided, draggable blocks can be dragged vertically to a new start time on the same day. The new start snaps to snapMinutes (nearest gridline) and is clamped to [workStart, workEnd]; duration is preserved by the caller. Use canDrag to restrict which blocks move (e.g. read-only overlays stay fixed):

<DayTimeline
  date={date}
  appointments={appts}
  onReschedule={(id, hhmm) => save(id, hhmm)}
  canDrag={(a) => a.editable}
  snapMinutes={15}
  labels={labels}
/>

findConflicts(startTime, duration, appointments, options?)

Returns the existing appointments that overlap a proposed slot.

findConflicts("09:30", 60, appointments); // session-only overlap

| Option | Type | Default | Notes | |---|---|---|---| | excludeId | string | — | Skip this appointment by id (useful when editing) | | buffer | number | 0 | Cleanup minutes reserved after each session. 0 keeps cleanup non-blocking, so back-to-back bookings are allowed | | getBuffer | (apt: T) => number | — | Per-appointment buffer for existing appointments; overrides buffer for each. The proposed slot still uses the scalar buffer for its own trailing cleanup |

// Enforce a 15-minute gap, except where an appointment opts out of cleanup:
findConflicts("10:00", 60, appointments, {
  buffer: 15,
  getBuffer: (apt) => (apt.skip_cleanup ? 0 : 15),
});

generateSlots(options)

Generate the bookable start-time grid for a single day. Candidate starts run from workStart by step minutes while the session ends by workEnd; the trailing buffer may spill past close. Availability is computed with findConflicts, so buffer semantics match exactly.

// One procedure, fixed 15-min cleanup
generateSlots({ date, appointments, duration: 60, step: 30, buffer: 15 });

// Procedure carries its own duration + break (may be 0)
generateSlots({
  date, appointments,
  duration: selected.duration,
  step: 15,
  buffer: selected.break,
  getBuffer: (apt) => apt.break ?? 0,
});

Returns Slot[] — every candidate, in order: { time: "HH:MM"; available: boolean; conflictIds: string[] }.

| Option | Type | Default | Notes | |---|---|---|---| | date | string (YYYY-MM-DD) | — | Day to generate for; appointments are filtered to it | | appointments | T[] | — | Existing appointments (any date; filtered internally) | | duration | number | — | Session length being booked, minutes | | step | number | — | Minutes between candidate starts | | workStart / workEnd | number | 9 / 21 | Opening/closing hour | | buffer | number | 0 | Trailing cleanup for the booked slot | | getBuffer | (apt: T) => number | — | Per-appointment buffer for existing appointments | | excludeId | string | — | Skip this appointment — for rescheduling |

Labels

interface Labels {
  today: string;
  weekdays: [string, string, string, string, string, string, string]; // Mo..Su
  monthYear?: (year, month) => string;
  appointmentsCount?: (n) => string;
  showEarlyHours?: (workStart) => string;
  hideEarlyHours?: string;
  showLateHours?: (workEnd) => string;
  hideLateHours?: string;
  schedule?: string;
  loading?: string;
}

License

MIT