Scheduler
A themeable, accessible day/week/month calendar for events and appointments — one headless core, many views, client- or server-driven, in React (@bpdm/scheduler) and Angular (@bpdm/ng-scheduler).
The Scheduler lays events onto a time grid or month grid. Overlapping events
cascade automatically, the current time is marked, and every view is driven by one
framework-agnostic core (@bpdm/scheduler-core). It reads events from a data
source — an in-memory array on the client, or your own source that fetches a
visible range from a server. Zero runtime dependencies in the core; each binding is
a few kilobytes.
Ships for React (@bpdm/scheduler) and Angular (@bpdm/ng-scheduler) — the
same headless core, so the model, views, options and responsive behaviour match. The
tabs below show each. Still pre-1.0: minor versions may adjust the API.
What you can configure
Everything below is built in — you opt into the interactive bits with a prop:
- Data — a client array (
events) or a server source (dataSource, range-based). → Quick start · Data source - Views —
defaultView, which views the toolbar offers (views), the visible hours (dayStartHour/dayEndHour/scrollToHour), first day of week (weekStartsOn), viewport height (maxHeight), row density (hourHeight), month chips per day (monthMaxChips). → Views - Creating events — click an empty slot to open your own form (
renderCreateForm), persist withonCreate; tune the proposed slot (createDuration,snapMinutes,createDefaultHour) or just get the raw slot (onSelectSlot). → Creating events - Opening events — a built-in detail dialog by default, or take over with
onEventClick. The month “+N more” day peek and its ‹ Back navigation are automatic. → Event details · Month - Editing events — set
onEventChangeto enable drag-to-move / drag-to-resize (keyboard-operable + announced);editabletoggles it. → Editing events - Internationalization — every string via
messages, dates/times vialocale, RTL automatically. → Internationalization - Theming — the four
@bpdm/tokensthemes viadata-theme; event colours viacategory. → Theming
See the full prop table for defaults and types.
Install
pnpm add @bpdm/schedulerImport the stylesheet once (it reads the @bpdm/tokens CSS variables, so the four
themes and RTL come for free):
// e.g. in your root layout or main entry
import '@bpdm/scheduler/styles.css';pnpm add @bpdm/ng-schedulerAdd the stylesheet once in angular.json (or import it in your global styles.css):
{
"styles": ["@bpdm/ng-scheduler/styles.css", "src/styles.css"]
}Quick start — client-side events
Pass an array of events. Each event needs id, title, start and end; an
optional category picks a themeable colour, and location / description show in
the built-in detail dialog.
import { Scheduler, type CalendarEvent } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
const events: CalendarEvent[] = [
{
id: '1',
title: 'Sprint planning',
start: new Date(2026, 6, 20, 10, 0),
end: new Date(2026, 6, 20, 11, 30),
category: 'amber',
location: 'Room Aster',
description: 'Plan the sprint backlog and lock the two-week goal.',
},
{
id: '2',
title: 'Design review',
start: new Date(2026, 6, 21, 11, 0),
end: new Date(2026, 6, 21, 12, 30),
category: 'teal',
},
{
id: '3',
title: 'Standup',
start: new Date(2026, 6, 22, 9, 30),
end: new Date(2026, 6, 22, 9, 45),
category: 'blue',
},
];
export function TeamCalendar() {
return (
<Scheduler
events={events}
defaultView="week"
onEventClick={(event) => console.log('clicked', event.id)}
/>
);
}import { Component } from '@angular/core';
import { BpdmScheduler, type CalendarEvent } from '@bpdm/ng-scheduler';
@Component({
selector: 'app-team-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `
<bpdm-scheduler
[events]="events"
defaultView="week"
(eventClick)="onEventClick($event)"
/>
`,
})
export class TeamCalendarComponent {
events: CalendarEvent[] = [
{
id: '1',
title: 'Sprint planning',
start: new Date(2026, 6, 20, 10, 0),
end: new Date(2026, 6, 20, 11, 30),
category: 'amber',
location: 'Room Aster',
description: 'Plan the sprint backlog and lock the two-week goal.',
},
{
id: '2',
title: 'Design review',
start: new Date(2026, 6, 21, 11, 0),
end: new Date(2026, 6, 21, 12, 30),
category: 'teal',
},
{
id: '3',
title: 'Standup',
start: new Date(2026, 6, 22, 9, 30),
end: new Date(2026, 6, 22, 9, 45),
category: 'blue',
},
];
onEventClick(event: CalendarEvent) {
console.log('clicked', event.id);
}
}Views
Set defaultView and choose which the toolbar offers via views. Navigate with
Today / ‹ › — each step moves by the view's period (a day, a week, a month).
Day and Week
The time grid holds the whole day (00:00–24:00); it opens scrolled to scrollToHour
(7 AM by default) and scrolls to reach early-morning or evening events. Overlapping
events cascade — wider cards, offset and stacked, with the hovered one on top.
Month
The month grid spans only the weeks the month actually covers (5 or 6), with event chips per day and a “+N more” overflow. Scroll the grid up/down to move between months.
When a day has more events than fit, “+N more” opens a day peek — a scrollable popup listing every event on that day (its background is locked, so wheeling scrolls the list, not the page). Clicking an event there opens its detail dialog with a ‹ Back control that returns you to the list. All of this is built in and keyboard-accessible — nothing to wire up.
Responsive
The scheduler adapts to its own container's width — not just the browser window — entirely in CSS (container queries, zero JavaScript, SSR-safe), so it behaves the same on a phone as it does embedded in a narrow panel. Nothing to configure:
- Day / week keep a usable column width and scroll horizontally rather than squishing to slivers; the time gutter and day headers stay pinned.
- Month cells shorten and event chips collapse to dots on small widths — "+N more" still opens the day
peek. With
collapseToDayBelowset, a phone-width month also caps the dots (≤2) so cells stay compact. - Toolbar wraps the view switcher onto its own row, and touch pointers get larger hit areas.
A seven-column week is inherently cramped on a phone, so — like most calendar apps — you can have a week
become a single day with a week strip below a width you choose with collapseToDayBelow. The strip
shows the week's seven days (two-letter name + date, with a dot for days that have events); tap one to
switch, or step a day at a time with ‹ ›. It is opt-in (off by default, zero cost), keeps the store on
week (so the toolbar still reads Week and widening is seamless), and measures the scheduler's own
container, not the window. The demos on this page use collapseToDayBelow={640}; narrow the page (or view
it on a phone) to watch the compact week.
import { Scheduler } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
// On a container narrower than 640px, "week" renders as a single day.
<Scheduler events={events} defaultView="week" collapseToDayBelow={640} />;// <bpdm-scheduler [events]="events" defaultView="week" [collapseToDayBelow]="640" />Data source — server-side events
events builds an in-memory source for you. For server data, pass a dataSource
whose fetch returns the events for a visible range — it may return an array or a
promise, so a real API call drops straight in. The scheduler re-fetches whenever the
visible range changes, so the server only sends what's on screen. The same component
handles both; nothing else changes.
import { Scheduler, type CalendarEvent, type DataSource } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
// The API sends dates as ISO strings; revive them into Date objects on the way in.
interface EventRow {
id: string;
title: string;
start: string;
end: string;
category?: string;
}
const remote: DataSource = {
async fetch(range) {
const params = new URLSearchParams({
from: range.start.toISOString(),
to: range.end.toISOString(),
});
const res = await fetch(`/api/events?${params}`);
const rows: EventRow[] = await res.json();
return rows.map(
(row): CalendarEvent => ({
...row,
start: new Date(row.start),
end: new Date(row.end),
}),
);
},
};
export function ServerCalendar() {
return <Scheduler dataSource={remote} defaultView="week" />;
}import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { BpdmScheduler, type CalendarEvent, type DataSource } from '@bpdm/ng-scheduler';
interface EventRow {
id: string;
title: string;
start: string;
end: string;
category?: string;
}
@Component({
selector: 'app-server-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `<bpdm-scheduler [dataSource]="remote" defaultView="week" />`,
})
export class ServerCalendarComponent {
private http = inject(HttpClient);
remote: DataSource = {
fetch: async (range) => {
const rows = await firstValueFrom(
this.http.get<EventRow[]>('/api/events', {
params: {
from: range.start.toISOString(),
to: range.end.toISOString(),
},
}),
);
return rows.map(
(row): CalendarEvent => ({
...row,
start: new Date(row.start),
end: new Date(row.end),
}),
);
},
};
}Event details
By default, clicking an event opens a built-in detail dialog (title, time,
location, description). Supply onEventClick to handle clicks yourself — the
built-in dialog is then suppressed, so you can open your own editor or navigate.
import { useState } from 'react';
import { Scheduler, type CalendarEvent } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
export function EditableCalendar({ events }: { events: CalendarEvent[] }) {
const [selected, setSelected] = useState<CalendarEvent | null>(null);
return (
<>
<Scheduler events={events} onEventClick={setSelected} />
{selected && (
<div role="dialog" aria-label={selected.title}>
<h2>{selected.title}</h2>
{/* your own editor, navigation, delete button, … */}
<button onClick={() => setSelected(null)}>Close</button>
</div>
)}
</>
);
}import { Component, signal } from '@angular/core';
import { BpdmScheduler, type CalendarEvent } from '@bpdm/ng-scheduler';
@Component({
selector: 'app-editable-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `
<bpdm-scheduler [events]="events" (eventClick)="selected.set($event)" />
@if (selected(); as event) {
<div role="dialog" [attr.aria-label]="event.title">
<h2>{{ event.title }}</h2>
<!-- your own editor, navigation, delete button, … -->
<button (click)="selected.set(null)">Close</button>
</div>
}
`,
})
export class EditableCalendarComponent {
events: CalendarEvent[] = [];
selected = signal<CalendarEvent | null>(null);
}Creating events
The scheduler ships no form of its own — it stays dependency-free. Clicking an
empty slot (a time in day/week, a day in month) hands you the clicked range; you
render your own fields via renderCreateForm — any inputs, any component
library — and onCreate receives the finished event, which appears immediately.
The exact same wiring works client- or server-side.
Click an empty slot above to open the create popup. That form is built with
@bpdm/ui controls — but the scheduler itself needs nothing; you could use plain
inputs, as below.
Where does the time come from? A day/week click carries the exact clicked
time (snapped to snapMinutes). A month cell has no time axis, so a click there
proposes createDefaultHour (9 AM by default — set it to whatever you like). Either
way, slot.start/slot.end are just defaults — add a time field to your form (a
<select> below, the two dropdowns in the live demo above) and pass explicit
start/end to submit to override them.
import { useState } from 'react';
import { Scheduler, type CalendarEvent, type CreateFormArgs } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
// Your own form — plain inputs here; swap in any component library you like.
function CreateForm({ slot, submit, cancel }: CreateFormArgs) {
const [title, setTitle] = useState('');
// seed the time picker from the clicked slot (HH:MM)
const [time, setTime] = useState(
() => slot.start.toTimeString().slice(0, 5),
);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!title.trim()) return;
// override the slot's default time with the chosen one (1-hour event)
const [h, m] = time.split(':').map(Number);
const start = new Date(slot.start);
start.setHours(h, m, 0, 0);
const end = new Date(start.getTime() + 60 * 60 * 1000);
submit({ title: title.trim(), start, end, category: 'violet' });
}}
>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
<input type="time" value={time} onChange={(e) => setTime(e.target.value)} />
<button type="button" onClick={cancel}>Cancel</button>
<button type="submit">Create</button>
</form>
);
}
export function EditableCalendar() {
const [events, setEvents] = useState<CalendarEvent[]>([]);
return (
<Scheduler
events={events}
renderCreateForm={(args) => <CreateForm {...args} />}
onCreate={(event) => setEvents((prev) => [...prev, event])}
createDefaultHour={9} // month clicks propose 9 AM (day/week use the clicked time)
/>
);
}For server-side persistence, do the write in onCreate (e.g. via your
dataSource.create); once it resolves the scheduler refetches the visible range, so
the server's authoritative record (assigned id, normalized times) is what shows:
import { Scheduler, type CalendarEvent, type DataSource } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
const remote: DataSource = {
async fetch(range) {
const params = new URLSearchParams({
from: range.start.toISOString(),
to: range.end.toISOString(),
});
const res = await fetch(`/api/events?${params}`);
const rows: CalendarEvent[] = await res.json();
return rows.map((r) => ({ ...r, start: new Date(r.start), end: new Date(r.end) }));
},
async create(event) {
const res = await fetch('/api/events', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(event),
});
return res.json();
},
};
export function ServerCalendar() {
return (
<Scheduler
dataSource={remote}
renderCreateForm={(args) => <CreateForm {...args} />}
onCreate={(event) => remote.create?.(event)}
/>
);
}Prefer to own the whole popup? Skip renderCreateForm and use
onSelectSlot(slot) to open your own dialog anywhere in your app.
import { Component, signal } from '@angular/core';
import { BpdmScheduler, type CalendarEvent } from '@bpdm/ng-scheduler';
@Component({
selector: 'app-editable-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `
<bpdm-scheduler [events]="events()" (create)="onCreate($event)">
<ng-template #createForm let-slot="slot" let-submit="submit" let-cancel="cancel">
<!-- your own fields — call submit({ title, location, … }) or cancel() -->
</ng-template>
</bpdm-scheduler>
`,
})
export class EditableCalendarComponent {
events = signal<CalendarEvent[]>([]);
onCreate(event: CalendarEvent) {
this.events.update((prev) => [...prev, event]);
}
}Editing events
Pass onEventChange and events become draggable everywhere: in day/week drag the body to move
(snapped to snapMinutes; in week view, drag across days too) and the bottom edge to resize; in
month view drag a chip to another day. On drop you get the updated event to persist; the grid
refetches after, so a client array or a server dataSource.update both work.
import { useState } from 'react';
import { Scheduler, type CalendarEvent } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
export function EditableCalendar({ initial }: { initial: CalendarEvent[] }) {
const [events, setEvents] = useState(initial);
return (
<Scheduler
events={events}
onEventChange={(changed) =>
setEvents((prev) => prev.map((e) => (e.id === changed.id ? changed : e)))
}
/>
);
}import { Component, signal } from '@angular/core';
import { BpdmScheduler, type CalendarEvent } from '@bpdm/ng-scheduler';
@Component({
selector: 'app-editable-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `<bpdm-scheduler [events]="events()" (eventChange)="onChange($event)" />`,
})
export class EditableCalendarComponent {
events = signal<CalendarEvent[]>([]);
onChange(changed: CalendarEvent) {
this.events.update((prev) => prev.map((e) => (e.id === changed.id ? changed : e)));
}
}Fully keyboard-operable (no mouse required) via a grab mode, so the arrow keys never fight grid navigation. Focus an event and press Space to pick it up (announced), then:
- day/week — ↑ / ↓ move its time, ← / → move it to another day, Shift + ↑ / ↓ resize it
- month — ← / → move by a day, ↑ / ↓ by a week
Press Space (or Enter) to drop, or Escape to cancel. Enter on an event that isn't grabbed opens it. Each step snaps and is announced to screen readers — satisfying WCAG 2.1.1 (Keyboard) and 2.5.7 (Dragging Movements).
Theming
The scheduler is styled entirely with @bpdm/tokens CSS variables, so it adopts
whichever of the four themes (paper, mist, charcoal, slate) is active on an
ancestor via data-theme — no configuration. Event categories (amber, violet,
teal, blue, rose) mix with the surface colour, so they read on light and dark.
Try the theme switcher at the top of this page.
Internationalization
Every user-facing string is overridable, dates and times follow a locale, and the
layout is fully RTL via logical CSS.
import { Scheduler, type CalendarEvent } from '@bpdm/scheduler';
import '@bpdm/scheduler/styles.css';
const events: CalendarEvent[] = [
{
id: '1',
title: 'Sprintplanung',
start: new Date(2026, 6, 20, 10, 0),
end: new Date(2026, 6, 20, 11, 30),
category: 'amber',
},
];
export function GermanCalendar() {
return (
<Scheduler
events={events}
locale="de-DE"
weekStartsOn={1}
messages={{
today: 'Heute',
previous: 'Zurück',
next: 'Weiter',
day: 'Tag',
week: 'Woche',
month: 'Monat',
close: 'Schließen',
}}
/>
);
}locale drives day names, dates and the 12/24-hour clock; messages overrides the
labels. In an RTL document (dir="rtl") the grid, columns and dialog mirror
automatically.
Every string is translatable — not only the visible ones but also the assistive-tech
labels and announcements, so a localized calendar is fully localized for screen-reader users
too. Notable keys beyond the obvious today/day/week/month:
| Key | Used for |
|---|---|
viewLabel | aria-label of the view switcher |
gridLabel / monthLabel | aria-label of the time grid / month grid |
allDay | an all-day event's time slot |
more / showAll | the "+N more" chip text / its aria-label (showAll interpolates {count}) |
grabbed / dropped | announced when a keyboard drag picks up / releases an event |
movedTo / resizedTo | announced (with the new time) after a keyboard move / resize |
eventAdjustable | aria-roledescription of an editable event |
messages is a partial override — anything you omit falls back to the built-in English default,
so you only supply the strings you translate.
import { Component } from '@angular/core';
import { BpdmScheduler, type CalendarEvent } from '@bpdm/ng-scheduler';
@Component({
selector: 'app-german-calendar',
standalone: true,
imports: [BpdmScheduler],
template: `
<bpdm-scheduler
[events]="events"
locale="de-DE"
[weekStartsOn]="1"
[messages]="messages"
/>
`,
})
export class GermanCalendarComponent {
events: CalendarEvent[] = [
{
id: '1',
title: 'Sprintplanung',
start: new Date(2026, 6, 20, 10, 0),
end: new Date(2026, 6, 20, 11, 30),
category: 'amber',
},
];
messages = {
today: 'Heute',
previous: 'Zurück',
next: 'Weiter',
day: 'Tag',
week: 'Woche',
month: 'Monat',
close: 'Schließen',
};
}Accessibility
Built to WCAG 2.1 AA. What that means concretely:
- Structure (1.3.1 Info & Relationships). The grid uses ARIA
grid/row/gridcellroles; the view switcher is atablist; the detail dialog is a labelledrole="dialog"witharia-modal. Every interactive label is driven bymessages, so it localizes with the rest of the UI. - Name, role, value (4.1.2). Every event and chip is a real
<button>; editable events carryaria-roledescriptionandaria-keyshortcuts. - Keyboard (2.1.1) — no mouse required, and no trap (2.1.2). Full editing runs through a
grab mode: focus an event,
Spaceto pick it up, arrow keys to move (↑/↓ time, ←/→ day; month: ←/→ day, ↑/↓ week),Shift+arrows to resize,Space/Enterto drop,Escapeto cancel.Enteron an ungrabbed event opens it. Dialogs close onEscapeand take/restore focus. Arrow keys are inert until an event is grabbed, so they never fight grid navigation. - Dragging movements (2.5.7). The keyboard grab mode is a complete, single-pointer-free alternative to drag-and-drop.
- Status messages (4.1.3). Moves, resizes and pick-up/drop are announced through a polite
role="status"live region — nothing relies on seeing the drag. - Focus visible (2.4.7). Every control has a visible focus ring; after a keyboard move the moved event keeps focus so you can keep going.
- Contrast (1.4.3 / 1.4.11). Colors come from
@bpdm/tokens, which ship AA-contrast pairs across all four themes. - Reflow & motion (1.4.10 / 2.3.3). Layout is responsive, and all motion respects
prefers-reduced-motion.
Performance & scale
- Range-based data. Only the visible window is ever fetched and laid out. A
dataSourcereceives just{ from, to }, so a calendar backed by millions of stored rows stays light — the server returns only what's on screen, and the grid refetches when you navigate. - Overlap engine. Dense days are laid out by a cluster-detecting, lane-packing
algorithm in the core (
packEvents/layoutDay) — overlapping events cascade automatically; you never position anything by hand. - Small and tree-shakeable. The core (
@bpdm/scheduler-core) has zero runtime dependencies; the React binding is a few kilobytes brotli, ESM + CJS, and the budget is enforced in CI withsize-limit. - SSR-safe. Renders on the server and hydrates cleanly. Read "now"/live dates on the client (e.g. in an effect) to avoid a server/client mismatch; the component itself does no non-deterministic work during render.
Testing
The scheduler is tested at two levels, and you can test your own integration the same way.
How it's tested internally
@bpdm/scheduler-core— pure unit tests for the date/time math, the overlap → lane-packing layout engine, and the observable store. No DOM, fully deterministic.@bpdm/scheduler— component tests with Vitest + Testing Library in ajsdomenvironment. Coverage includes view rendering, the create/detail dialogs, the "+N more" day peek, the full keyboard grab-mode (pick up, move, resize, cross-cell move,Escaperelease, and that arrows are inert until grabbed), focus restoration after a move remounts the event, andmessagesoverrides (i18n).- Every change is gated in CI:
pnpm test(unit + component),pnpm lint,pnpm build, and asize-limitbudget so the bundle can't silently grow.
Testing your own usage
Because events and chips are real buttons with accessible names, you query and drive the scheduler the way a user would — by role and name, then keyboard:
import { render, screen, fireEvent } from '@testing-library/react';
import { useState } from 'react';
import { Scheduler, type CalendarEvent } from '@bpdm/scheduler';
function Harness() {
const [events, setEvents] = useState<CalendarEvent[]>([
{ id: 'a', title: 'Sprint planning', start: new Date(2026, 6, 20, 9, 0), end: new Date(2026, 6, 20, 10, 0) },
]);
return (
<Scheduler
events={events}
defaultDate={new Date(2026, 6, 20)}
views={['day']}
onEventChange={(changed) =>
setEvents((prev) => prev.map((e) => (e.id === changed.id ? changed : e)))
}
/>
);
}
test('an event can be moved later with the keyboard', () => {
render(<Harness />);
const event = screen.getByRole('button', { name: /Sprint planning/ });
event.focus();
fireEvent.keyDown(event, { key: ' ' }); // pick up (grab mode)
fireEvent.keyDown(event, { key: 'ArrowDown' }); // move to a later time
// assert against your own persisted state / onEventChange spy
});Note the Space before the arrow — nothing moves until an event is grabbed, exactly as a
keyboard user experiences it.
API
<Scheduler> props
| Prop | Type | Default | Description |
|---|---|---|---|
events | CalendarEvent[] | [] | Client-side events (ignored if dataSource is set). |
dataSource | DataSource | — | Custom source, e.g. server-backed. |
defaultView | 'day' | 'week' | 'month' | 'week' | Initial view. |
defaultDate | Date | today | Initial focused date. |
views | ViewType[] | ['day','week','month'] | Views the toolbar offers. |
dayStartHour | number | 0 | First hour rendered (day/week). |
dayEndHour | number | 24 | Last hour rendered (day/week). |
scrollToHour | number | 7 | Hour the viewport opens at. |
maxHeight | number | 600 | Day/week viewport height (px) before it scrolls. |
weekStartsOn | 0–6 | 1 | First day of the week (Monday). |
now | Date | new Date() | Reference time (today marker + now line). |
locale | string | runtime | BCP-47 locale for dates/times. |
messages | Partial<SchedulerMessages> | English | Override UI strings. |
onEventClick | (event) => void | — | Handle clicks yourself; suppresses the built-in dialog. |
editable | boolean | !!onEventChange | Enable drag-to-move / drag-to-resize in day/week. |
onEventChange | (event) => void | Promise<void> | — | Persist a moved/resized event (update state, or dataSource.update); the grid refetches after. |
onSelectSlot | (slot: SlotSelection) => void | — | Fires when an empty slot is clicked (its start/end). |
renderCreateForm | (args: CreateFormArgs) => ReactNode | — | Render your own create form in the popup on slot click. |
onCreate | (event) => void | Promise<void> | — | Persist a created event (update state, or POST); the grid refetches after. |
createDuration | number | 60 | Minutes a click proposes for the new slot. |
snapMinutes | number | 30 | Snap a clicked day/week time to this many minutes. |
createDefaultHour | number | 9 | Month cells have no time axis — a click there proposes this hour (0–23). |
hourHeight | number | 52 | Pixel height of one hour row in day/week (row density). |
monthMaxChips | number | 3 | Event chips shown per day cell in month view before “+N more”. |
collapseToDayBelow | number | — | Opt-in phone layout: below this container width (px), a week becomes a single day with a week strip, and the month caps its event dots lower (≤2). Measures the scheduler's own width, not the window. Off (and free) when unset. |
className | string | — | Extra class on the root. |
CalendarEvent
interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
allDay?: boolean;
resourceId?: string; // multi-resource views (roadmap)
category?: string; // 'amber' | 'violet' | 'teal' | 'blue' | 'rose'
location?: string; // shown in the dialog
description?: string; // shown in the dialog
data?: unknown; // your own payload, untouched
}DataSource
interface DataSource<E = CalendarEvent> {
fetch(range: { start: Date; end: Date }, options?: { resourceIds?: string[] }): E[] | Promise<E[]>;
create?(event: E): E | Promise<E>;
update?(event: E): E | Promise<E>;
remove?(id: string): void | Promise<void>;
}InMemoryDataSource implements this for client-side arrays and is what events
uses under the hood.
Create types
interface SlotSelection {
start: Date;
end: Date;
}
// what a renderCreateForm submits — id is generated if omitted; start/end
// default to the clicked slot; the rest map onto CalendarEvent.
interface CreateEventInput {
id?: string;
title: string;
start?: Date;
end?: Date;
category?: string;
location?: string;
description?: string;
data?: unknown;
}
interface CreateFormArgs {
slot: SlotSelection;
submit: (input: CreateEventInput) => void;
cancel: () => void;
}Roadmap: drag to create / move / resize, resources (multi-person columns),
Timeline / Agenda / Year views, recurrence (RRULE) and time zones — all on the
same core, plus the Angular binding and a pnpm create / @bpdm/cli add scheduler
installer.
Spinner
A loading indicator in six looks — sizes xs–xl, colour-inheriting, with a LoadingOverlay, in React (@bpdm/ui) and Angular (@bpdm/ng).
One design system across React and Angular
A practical guide to sharing one design system across React and Angular, and any framework that reads CSS variables. The token-driven approach behind the bpdm/ui component libraries.