Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions apps/docs/content/components/(chatbot)/day-calendar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: Day Calendar
description: A read-only day view calendar for visualizing scheduled events on a time grid. Designed for AI agents that need to confirm event scheduling to users.
path: elements/components/day-calendar
---

The `DayCalendar` component renders a single-day time grid with positioned event blocks. It auto-focuses the visible window around events and shows the current time indicator when viewing today.

<Preview path="day-calendar" />

## Installation

<ElementsInstaller path="day-calendar" />

## Features

- Auto-computes visible time window based on event times (padded by 2 hours)
- Current time indicator line when viewing today's date
- Percentage-based event block positioning for accurate layout
- Dark mode support via Tailwind CSS variables
- Compound component pattern for flexible composition
- Fully typed with TypeScript
- No external date library — uses `Intl.DateTimeFormat`

## Props

### `<DayCalendar />`

<TypeTable
type={{
date: {
description: "The date to display.",
type: "Date",
},
events: {
description: "List of events to render on the time grid.",
type: "CalendarEvent[]",
},
startHour: {
description:
"Override the start hour of the visible window (0–24). When omitted, auto-computed from events.",
type: "number",
},
endHour: {
description:
"Override the end hour of the visible window (0–24). When omitted, auto-computed from events.",
type: "number",
},
"...props": {
description: "Any other props are spread to the root div element.",
type: "React.HTMLAttributes<HTMLDivElement>",
},
}}
/>

### `<DayCalendarHeader />`

<TypeTable
type={{
children: {
description:
'Custom header content. Defaults to formatted date string (e.g. "Thursday, February 27, 2025").',
type: "React.ReactNode",
},
"...props": {
description: "Any other props are spread to the root div element.",
type: "React.HTMLAttributes<HTMLDivElement>",
},
}}
/>

### `<DayCalendarContent />`

<TypeTable
type={{
"...props": {
description: "Any other props are spread to the root div element.",
type: "React.HTMLAttributes<HTMLDivElement>",
},
}}
/>

### `<DayCalendarEvent />`

<TypeTable
type={{
event: {
description: "The calendar event data to display.",
type: "CalendarEvent",
},
startHour: {
description:
"The start hour of the visible window, used for positioning.",
type: "number",
},
totalHours: {
description: "Total visible hours, used for sizing.",
type: "number",
},
"...props": {
description: "Any other props are spread to the root div element.",
type: "React.HTMLAttributes<HTMLDivElement>",
},
}}
/>

### `CalendarEvent`

<TypeTable
type={{
start: {
description: "Event start time.",
type: "Date",
},
end: {
description: "Event end time.",
type: "Date",
},
title: {
description: "Event title displayed in the event block.",
type: "string",
},
description: {
description: "Optional description shown below the time range.",
type: "string",
},
}}
/>
162 changes: 162 additions & 0 deletions packages/elements/__tests__/day-calendar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { render, screen } from "@testing-library/react";

import {
DayCalendar,
DayCalendarContent,
DayCalendarEvent,
DayCalendarHeader,
type CalendarEvent,
} from "../src/day-calendar";

const date = new Date("2025-02-27T00:00:00");

const events: CalendarEvent[] = [
{
description: "Daily sync",
end: new Date("2025-02-27T17:00:00"),
start: new Date("2025-02-27T16:00:00"),
title: "Team Standup",
},
];

describe("dayCalendar", () => {
it("renders children", () => {
render(
<DayCalendar date={date} events={[]}>
<span>Content</span>
</DayCalendar>
);
expect(screen.getByText("Content")).toBeInTheDocument();
});

it("throws error when sub-component used outside provider", () => {
const spy = vi.spyOn(console, "error").mockImplementation(vi.fn());

expect(() => render(<DayCalendarHeader />)).toThrow(
"DayCalendar components must be used within DayCalendar"
);

spy.mockRestore();
});
});

describe("dayCalendarHeader", () => {
it("renders formatted date", () => {
render(
<DayCalendar date={date} events={[]}>
<DayCalendarHeader />
</DayCalendar>
);

expect(screen.getByText("Thursday, February 27, 2025")).toBeInTheDocument();
});

it("renders custom children instead of formatted date", () => {
render(
<DayCalendar date={date} events={[]}>
<DayCalendarHeader>Custom Header</DayCalendarHeader>
</DayCalendar>
);

expect(screen.getByText("Custom Header")).toBeInTheDocument();
expect(
screen.queryByText("Thursday, February 27, 2025")
).not.toBeInTheDocument();
});
});

describe("dayCalendarContent", () => {
it("renders time grid", () => {
render(
<DayCalendar date={date} events={[]} endHour={11} startHour={9}>
<DayCalendarContent />
</DayCalendar>
);

expect(screen.getByText("10 AM")).toBeInTheDocument();
expect(screen.getByText("11 AM")).toBeInTheDocument();
});

it("renders event titles", () => {
render(
<DayCalendar date={date} events={events}>
<DayCalendarContent />
</DayCalendar>
);

expect(screen.getByText("Team Standup")).toBeInTheDocument();
});

it("renders event description", () => {
render(
<DayCalendar date={date} events={events}>
<DayCalendarContent />
</DayCalendar>
);

expect(screen.getByText("Daily sync")).toBeInTheDocument();
});

it("defaults to 9am-5pm when no events", () => {
render(
<DayCalendar date={date} events={[]}>
<DayCalendarContent />
</DayCalendar>
);

expect(screen.getByText("10 AM")).toBeInTheDocument();
expect(screen.getByText("5 PM")).toBeInTheDocument();
});
});

describe("dayCalendarEvent", () => {
it("renders event title and time range", () => {
render(
<DayCalendar date={date} events={events}>
<DayCalendarEvent
event={events[0]}
startHour={14}
totalHours={6}
/>
</DayCalendar>
);

expect(screen.getByText("Team Standup")).toBeInTheDocument();
expect(screen.getByText(/4:00 PM/)).toBeInTheDocument();
expect(screen.getByText(/5:00 PM/)).toBeInTheDocument();
});

it("renders description when provided", () => {
render(
<DayCalendar date={date} events={events}>
<DayCalendarEvent
event={events[0]}
startHour={14}
totalHours={6}
/>
</DayCalendar>
);

expect(screen.getByText("Daily sync")).toBeInTheDocument();
});

it("omits description when not provided", () => {
const eventNoDesc: CalendarEvent = {
end: new Date("2025-02-27T17:00:00"),
start: new Date("2025-02-27T16:00:00"),
title: "No Desc Event",
};

render(
<DayCalendar date={date} events={[eventNoDesc]}>
<DayCalendarEvent
event={eventNoDesc}
startHour={14}
totalHours={6}
/>
</DayCalendar>
);

expect(screen.getByText("No Desc Event")).toBeInTheDocument();
});
});
Loading