@marianmeres/calendar-utils
v2.0.0
Published
[](https://jsr.io/@marianmeres/calendar-utils) [](https://www.npmjs.com/package/@marianmeres/calendar-utils) [ lookup, atomic batch / replaceAll, bounded cost for pathological spans
- Multi-day + all-day events with placement info for rendering
- Overlap helpers — hour-precision conflict queries and column layout for side-by-side events
- Strict validation — UTC/offset enforced for timed events; rejects ambiguous inputs
- DST-safe — correct in zones whose day starts at 01:00 on the transition day
- Regional weeks — any first day of the week and configurable weekend days
Installation
# Deno
deno add jsr:@marianmeres/calendar-utils
# npm
npm install @marianmeres/calendar-utilsQuick Start
import {
type CalendarEvent,
createCalendarEvents,
createMonthView,
getWeekdayHeaders,
layoutOverlappingEvents,
} from "@marianmeres/calendar-utils";
// Month view with reactive grid
const view = createMonthView({ year: 2024, month: 1 }, { zone: "UTC" });
const headers = getWeekdayHeaders(1, "short"); // ["Mon", ..., "Sun"]
view.subscribe(({ grid, monthLabel }) => {
// grid is DayCell[][] — render however you like
});
// Event store with day-indexed lookup
interface MyEvent extends CalendarEvent {
title: string;
color: string;
}
const events = createCalendarEvents<MyEvent>([], { zone: "UTC" });
events.add({
id: "1",
start: "2024-01-15T09:00:00Z", // timed events MUST include Z or ±HH:MM
end: "2024-01-17T17:00:00Z",
title: "Conference",
color: "blue",
});
// O(events on day) day query — placement info included
const placements = events.getForDay("2024-01-16");
// [{ event, isStart: false, isEnd: false, dayIndex: 1, spanDays: 3 }]
// All-day events use date-only ISO and are zone-independent
events.add({
id: "2",
start: "2024-01-20",
end: "2024-01-22",
allDay: true,
title: "Holiday",
color: "red",
});
// Hour-precision conflict check (half-open window, touching does not count)
const busy = events.getOverlapping("2024-01-15T14:00:00Z", "2024-01-15T15:00:00Z");
// Side-by-side layout for overlapping events
const dayEvents = placements.map((p) => p.event);
const slots = layoutOverlappingEvents(dayEvents, { zone: "UTC" });
// → [{ event, column, columnCount, groupId }] — render with left = column / columnCountBulk operations (single subscriber notification)
events.replaceAll(serverEvents); // atomic — pre-validates, swap, one notify
events.batch(() => { // arbitrary sequence, one notify, rollback on throw
events.removeMany(deletedIds);
events.addMany(newEvents);
events.updateEvent("foo", { title: "Renamed" });
});API
See API.md for the complete reference. See AGENTS.md for the agent-oriented architecture and conventions guide.
Configuration
interface ZoneConfig {
zone?: string; // IANA timezone, default "local"
}
interface GridConfig extends ZoneConfig {
weekStartsOn?: Weekday; // 1 = Monday (default) … 7 = Sunday; any weekday allowed
weekendDays?: Weekday[]; // default [6, 7]
}
interface LoggingConfig {
debug?: boolean;
logger?: Logger; // stores only — pure helpers never log
}Input rules
Every date argument accepts a DateTime, a Date or an ISO string. A DateTime or Date is an instant and is converted into the configured zone; a bare ISO date is wall-clock in that zone.
Pure helpers and store constructors throw on unparseable input. Store mutators and navigation methods warn, leave the state untouched and return false; store queries warn and return an empty result.
Event format contract
| Shape | start / end | Meaning |
| ----------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
| Timed (allDay false/absent) | ISO 8601 (YYYY-MM-DDTHH:mm…) with mandatory Z or ±HH[:MM] | Exact instants, bucketed into the display zone |
| All-day (allDay: true) | YYYY-MM-DD or ISO with TZ designator (date portion as written used) | Zone-independent inclusive [startDate, endDate] |
Bare local-time strings (e.g. "2024-01-15T09:00:00") and time-only strings are rejected by validateEvent. Events are treated as immutable once added — use updateEvent to change them.
