# Calendar

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/calendar
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=804-5708

Review deadlines across entities, inspect a record without leaving the calendar, and start a filing or task for a specific day.

## Month [#month]


```tsx
import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarDefault = () => (
  <Calendar
    label="Compliance calendar"
    events={calendarEvents}
    dateConfig={{ today: '2026-10-13' }}
    viewConfig={{ defaultValue: 'month' }}
  />
);
```


Click a filing to inspect its details. October 13 has five records: **2 more** opens all five; choosing a record replaces that list inside the same popup. Escape closes the preview and returns focus to the originating trigger. Today, previous and next navigate the selected month.

The month has five or six weeks. Adjacent-month dates are muted but their events retain their colors. Weekend cells have a quiet background. Today uses the red date badge. At most three chips appear per day.

## Choosing the Component [#choosing-the-component]

| You need to…                                       | Use                                        |
| -------------------------------------------------- | ------------------------------------------ |
| Review records by month or in a chronological list | **Calendar**                               |
| Enter a date, month or date range in a form        | [DatePicker](/docs/components/date-picker) |
| Sort and filter records with many columns          | [Table](/docs/components/table)            |

Calendar owns presentation and interaction. Your application owns records, permissions, status calculations and persistence. Dates represent whole days; time slots, recurrence and drag scheduling are outside this component's contract.

## Usage [#usage]

```tsx
import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';

const events: CalendarEvent[] = [
  {
    id: 'vat-q3',
    title: 'VAT return Q3',
    date: '2026-10-20',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: { id: 'de', label: 'Meridian GmbH' },
    status: { label: 'In progress', tone: 'info' },
  },
];

<Calendar
  label="Compliance calendar"
  events={events}
  dateConfig={{ today: '2026-10-13' }}
/>;
```

Import `@commenda/pathfinder/styles.css` once in the application. No consumer Tailwind compilation is needed. Interactive callbacks belong in a client component in Next.js App Router.

## Agenda [#agenda]


```tsx
import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarAgenda = () => (
  <Calendar
    label="Upcoming deadlines"
    events={calendarEvents}
    dateConfig={{ today: '2026-10-13' }}
    viewConfig={{ defaultValue: 'agenda' }}
  />
);
```


Agenda groups the selected month's records by date. Entity flags, category and status remain visible. Status, assignee and action slots retain their alignment when a record has no assignee. Actions are available on hover, keyboard focus and touch. Narrow containers move trailing details to a second line.

## Controlled State [#controlled-state]


```tsx
import { useState } from 'react';

import { Button } from '@commenda/pathfinder/button';
import { Calendar, type CalendarView } from '@commenda/pathfinder/calendar';

export const CalendarDataControlled = () => {
  const [month, setMonth] = useState('2026-10');
  const [view, setView] = useState<CalendarView>('agenda');

  return (
    <>
      <Button
        variant="secondary"
        size="sm"
        onClick={() => {
          setMonth('2026-10');
          setView('auto');
        }}
      >
        Reset to October and automatic view
      </Button>
      <Calendar
        label="Controlled deadlines"
        dateConfig={{ today: '2026-10-13' }}
        events={[
          {
            id: 'annual',
            title: 'Annual report',
            date: '2026-10-20',
            category: {
              id: 'entity',
              label: 'Entity compliance',
              tone: 'purple',
            },
            entity: { id: 'us', label: 'Meridian US Inc.' },
          },
        ]}
        monthConfig={{ value: month, onValueChange: setMonth }}
        viewConfig={{ value: view, onValueChange: setView }}
      />
      <output aria-live="polite">
        Application state: {month} · {view}
      </output>
    </>
  );
};
```


The text below the example shows application state. Navigate or change view, then use Reset. This is the pattern for URL state, saved preferences or a parent-owned toolbar.

Use `defaultValue` for internal state with an optional change notification. Use `value` **with** `onValueChange` when the parent owns state. Calendar emits the requested value; it does not override a controlled prop. Supplying both modes is a TypeScript error.

## Responsive View [#responsive-view]


```tsx
import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarResponsive = () => (
  <Calendar
    label="Responsive calendar"
    events={calendarEvents}
    dateConfig={{ today: '2026-10-13' }}
  />
);
```


The default `auto` view uses Agenda below **900px of container width** and Month at or above it. Choosing Month or Agenda makes that choice persistent. Set a controlled `viewConfig.value` to `auto` to restore automatic behavior. An explicitly selected Month view scrolls within its container on narrow screens.

## Entity Filtering [#entity-filtering]


```tsx
import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarEntities = () => (
  <Calendar
    label="Entity deadlines"
    events={calendarEvents}
    dateConfig={{ today: '2026-10-13' }}
    entityFilterConfig={{ entities: calendarEntities }}
    viewConfig={{ defaultValue: 'agenda' }}
  />
);
```


Empty selected IDs mean all entities. Choose one entity to remove its repeated name from agenda metadata; flags remain. Filter selection survives month and view changes. The optional configuration follows Pathfinder's `value/onValueChange` or `defaultValue` pattern. Supply the complete available entity choices and stable IDs.

## Categories and Statuses [#categories-and-statuses]

Category and lifecycle status are independent. The examples use these verified Figma bar modes:

| Category          | `category.tone` |
| ----------------- | --------------- |
| Direct tax        | `amber`         |
| Indirect tax      | `blue`          |
| Transfer pricing  | `indigo`        |
| Entity compliance | `purple`        |
| Payroll           | `teal`          |
| Customs           | `rose`          |
| Other             | `neutral`       |

`status.label` and `status.tone` describe the agenda badge. Optional `status.appearance` controls the compact record presentation:

| Appearance | Behavior                                                    |
| ---------- | ----------------------------------------------------------- |
| `default`  | Neutral chip with category bar                              |
| `overdue`  | Category bar retained; red status mark with Overdue tooltip |
| `filed`    | Muted category bar and text; green Completed mark           |
| `extended` | Dashed neutral outline                                      |

The revised October 12 Figma treatment supersedes older tinted-chip descriptions. The package never infers status from `today`. For example, an extended deadline can retain an “In progress” badge while using `appearance: 'extended'`. A flag is an optional React element; Pathfinder gives it optical 12px dimensions. Use the circular country flag assets.

## Record Previews and Actions [#record-previews-and-actions]


```tsx
import { useState } from 'react';

import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarPreview = () => {
  const [opened, setOpened] = useState('');

  return (
    <>
      <Calendar
        label="Record previews"
        events={calendarEvents}
        dateConfig={{ today: '2026-10-13' }}
        viewConfig={{ defaultValue: 'agenda' }}
        eventConfig={{
          onOpen: (event) => {
            setOpened(`Navigate to record ${event.id}: ${event.title}`);
          },
          getMenu: (event) => ({
            groups: [
              {
                id: 'actions',
                items: [
                  {
                    id: 'edit',
                    label: 'Edit deadline',
                    type: 'action',
                    onClick: () => {
                      setOpened(`Edit ${event.title}`);
                    },
                  },
                ],
              },
            ],
          }),
        }}
      />
      <output aria-live="polite">{opened}</output>
    </>
  );
};
```


Clicking a record opens its preview. Navigation occurs only through the explicit Open record action. `eventConfig.getMenu(event)` supplies the existing recursive Dropdown menu contract for record actions. All records retain their stable IDs when filtering or navigating.


```tsx
import { Calendar } from '@commenda/pathfinder/calendar';

export const CalendarDataCustomPreview = () => (
  <Calendar
    label="Task calendar"
    dateConfig={{ today: '2026-10-13', weekStartsOn: 1, locale: 'en-GB' }}
    events={[
      {
        id: 'task',
        title: 'Review supporting documents',
        date: '2026-10-13',
        category: { id: 'task', label: 'Task', tone: 'neutral' },
        entity: { id: 'us', label: 'Meridian US Inc.' },
      },
    ]}
    eventConfig={{
      renderPreview: (event) => (
        <div>
          <p>{event.entity.label}</p>
          <p>
            Application-owned task details, attachments and permissions go here.
          </p>
        </div>
      ),
    }}
  />
);
```


`renderPreview(event)` replaces the default metadata body. Calendar retains the accessible popup title, close control and focus return. Applications own authorization and save flows. Keep critical information readable without relying on color or a tooltip.

## Create on a Date [#create-on-a-date]


```tsx
import { useState } from 'react';

import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar';
import { DEFlag } from '@commenda/pathfinder/flags/DEFlag';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';

const calendarEntities = [
  { id: 'us', label: 'Meridian US Inc.', flag: <USFlag /> },
  { id: 'de', label: 'Meridian GmbH', flag: <DEFlag /> },
];

const calendarEvents: CalendarEvent[] = [
  {
    id: '1',
    title: 'Corporate income tax',
    date: '2026-10-13',
    category: { id: 'direct', label: 'Direct tax', tone: 'amber' },
    entity: calendarEntities[0],
    status: { label: 'Overdue', tone: 'error', appearance: 'overdue' },
    assignee: { kind: 'person', name: 'Priya Shah' },
  },
  {
    id: '2',
    title: 'VAT return Q3',
    date: '2026-10-13',
    category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' },
    entity: calendarEntities[1],
    status: { label: 'In progress', tone: 'info' },
  },
  {
    id: '3',
    title: 'Transfer pricing report',
    date: '2026-10-13',
    category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' },
    entity: calendarEntities[1],
    status: { label: 'Filed', tone: 'success', appearance: 'filed' },
  },
  {
    id: '4',
    title: 'Annual report',
    date: '2026-10-13',
    category: { id: 'entity', label: 'Entity compliance', tone: 'purple' },
    entity: calendarEntities[0],
    status: { label: 'Extended', tone: 'neutral', appearance: 'extended' },
  },
  {
    id: '5',
    title: 'Payroll withholding',
    date: '2026-10-13',
    category: { id: 'payroll', label: 'Payroll', tone: 'teal' },
    entity: calendarEntities[0],
    status: { label: 'To do', tone: 'neutral' },
  },
  {
    id: '6',
    title: 'Import declaration',
    date: '2026-10-20',
    category: { id: 'customs', label: 'Customs', tone: 'rose' },
    entity: calendarEntities[1],
    status: { label: 'Due soon', tone: 'warning' },
    assignee: { kind: 'person', name: 'Lucas Moreau' },
  },
  {
    id: '7',
    title: 'Board review',
    date: '2026-10-28',
    category: { id: 'other', label: 'Other', tone: 'neutral' },
    entity: calendarEntities[0],
  },
];

export const CalendarCreation = () => {
  const [message, setMessage] = useState(
    'Click a day background or use Add filing.'
  );

  return (
    <>
      <Calendar
        label="Create deadlines"
        events={calendarEvents}
        dateConfig={{ today: '2026-10-13' }}
        viewConfig={{ defaultValue: 'month' }}
        entityFilterConfig={{ entities: calendarEntities }}
        addConfig={{
          actions: [
            {
              id: 'filing',
              label: 'Add filing',
              onAction: ({ date }) => {
                setMessage(`Open the filing form with due date ${date}.`);
              },
            },
            {
              id: 'task',
              label: 'Add task',
              onAction: ({ date }) => {
                setMessage(`Open the task form with due date ${date}.`);
              },
            },
          ],
        }}
      />
      <output aria-live="polite">{message}</output>
    </>
  );
};
```


Click a day background or its plus affordance to choose an action. A temporary New chip appears while the menu is open; cancellation removes it. Each callback receives `{ date, entityIds }`. Clicking an existing record never invokes creation.

The toolbar uses the first action as its default and puts alternatives in a SplitButton. Toolbar creation uses the supplied business-local today; day creation uses the clicked date, including adjacent-month cells. The app opens the form and persists a new event. Calendar does not insert optimistic records automatically.

## API Loading and Recovery [#api-loading-and-recovery]


```tsx
import { useEffect, useState } from 'react';

import { Button } from '@commenda/pathfinder/button';
import {
  Calendar,
  type CalendarEvent,
  type CalendarVisibleRange,
} from '@commenda/pathfinder/calendar';

export const CalendarDataDefault = () => {
  const [range, setRange] = useState<CalendarVisibleRange>();
  const [response, setResponse] = useState<{
    key: string;
    events: CalendarEvent[];
  }>();
  const [failed, setFailed] = useState(false);
  const key = range ? `${range.start}/${range.end}` : '';
  useEffect(() => {
    if (!range || failed) return;
    const timer = setTimeout(() => {
      setResponse({
        key: `${range.start}/${range.end}`,
        events: [
          {
            id: range.month,
            title: 'API-loaded deadline',
            date: `${range.month}-15`,
            category: { id: 'tax', label: 'Direct tax', tone: 'amber' },
            entity: { id: 'company', label: 'Meridian US Inc.' },
          },
        ],
      });
    }, 500);

    return () => {
      clearTimeout(timer);
    };
  }, [range, failed]);

  return (
    <>
      <Button
        variant="secondary"
        size="sm"
        onClick={() => {
          setFailed(true);
        }}
      >
        Simulate API failure
      </Button>
      {range && (
        <p>
          Requested dates: {range.start} through {range.end} ({range.view}).
        </p>
      )}
      <Calendar
        label="API deadlines"
        dateConfig={{ today: '2026-10-13' }}
        events={response?.key === key ? response.events : []}
        onVisibleRangeChange={setRange}
        stateConfig={{
          isLoading: !failed && response?.key !== key,
          error: failed
            ? {
                message: 'Could not load deadlines.',
                onRetry: () => {
                  setResponse(undefined);
                  setFailed(false);
                },
              }
            : undefined,
        }}
      />
    </>
  );
};
```


This example simulates a request, discards obsolete results, and exposes failure/retry. Navigate rapidly or click Simulate API failure. Replace its timer with your query client or `fetch` and `AbortController`.

`onVisibleRangeChange` reports inclusive `{ start, end, month, view }`. Month includes adjacent grid dates; Agenda reports the selected month's first and last dates. Include selected entity IDs in the application query key. Cancel stale requests and only pass results belonging to the current key. Do not turn loading or failed responses into an empty event array without the corresponding `stateConfig`.

## Empty State [#empty-state]


```tsx
import { Calendar } from '@commenda/pathfinder/calendar';

export const CalendarDataEmpty = () => (
  <Calendar
    label="Empty calendar"
    events={[]}
    dateConfig={{ today: '2026-10-13' }}
    stateConfig={{ emptyContent: 'No deadlines match your current entities.' }}
  />
);
```


`stateConfig.emptyContent` explains an empty month or current entity filter. Month retains its date grid so creation remains available. Loading and retryable errors take precedence over empty content.

## API Reference [#api-reference]

Import `CalendarProps`, `CalendarEvent`, `CalendarEntity`, `CalendarView`, `CalendarTone`, `CalendarVisibleRange`, `CalendarAddContext` and `CalendarAddAction` from `@commenda/pathfinder/calendar` or the package root. The public API is one component with direct configuration props; day cells and popup components are internal.

| Prop                      | Type                                                 | Default / ownership                       |
| ------------------------- | ---------------------------------------------------- | ----------------------------------------- |
| `label`                   | `string`                                             | Required accessible region name           |
| `events`                  | `readonly CalendarEvent[]`                           | Required; app owns data and unique IDs    |
| `dateConfig.today`        | `string`                                             | Required `YYYY-MM-DD` business-local date |
| `dateConfig.locale`       | `string`                                             | `en-US`; dates use Intl formatting        |
| `dateConfig.weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6`                    | `0` (Sunday)                              |
| `monthConfig`             | `SelectionProps<string>`                             | Today’s `YYYY-MM` month                   |
| `viewConfig`              | `SelectionProps<CalendarView>`                       | `auto`; also `month` or `agenda`          |
| `entityFilterConfig`      | Selection config + `entities`, optional `label`      | Omitted: no filter; empty IDs: all        |
| `eventConfig`             | `renderPreview`, `onOpen`, `getMenu`                 | Default metadata preview                  |
| `addConfig.actions`       | `readonly CalendarAddAction[]`                       | Omitted: read-only calendar               |
| `stateConfig`             | `isLoading`, `loadingLabel`, `error`, `emptyContent` | Ready                                     |
| `onVisibleRangeChange`    | `(range: CalendarVisibleRange) => void`              | App-owned requests                        |
| `className`               | `string`                                             | Root styling only                         |

### Event Records [#event-records]

| Field      | Type                            | Guidance                                                                      |
| ---------- | ------------------------------- | ----------------------------------------------------------------------------- |
| `id`       | `string`                        | Stable and unique across the supplied collection                              |
| `title`    | `string`                        | Full record name; chips truncate, previews expose it                          |
| `date`     | `string`                        | Canonical `YYYY-MM-DD` deadline                                               |
| `category` | `{ id, label, tone }`           | Category identity, readable label and one of the seven bar tones              |
| `entity`   | `{ id, label, flag? }`          | Stable entity identity; optional circular flag element                        |
| `status`   | `{ label, tone?, appearance? }` | Badge content and independent compact-chip treatment                          |
| `assignee` | `PersonIdentity`                | `{ kind: 'person', userId?, name?, email?, src? }`; omission means unassigned |

Input order determines event order within each day. Agenda sorts day groups chronologically. The application should sort its records before passing them if a business-specific priority is required.

### Navigation and Entity Configs [#navigation-and-entity-configs]

| Config               | Fields                                                                   | Behavior                                             |
| -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- |
| `monthConfig`        | `value`, `onValueChange` **or** `defaultValue`, optional `onValueChange` | Values are `YYYY-MM`                                 |
| `viewConfig`         | Same controlled/uncontrolled shape                                       | `month`, `agenda` or `auto`                          |
| `entityFilterConfig` | Same shape with `readonly string[]`, plus `entities`, optional `label`   | Empty IDs means all; omitted config hides the filter |

`entities` is a readonly array of `{ id, label, flag? }`. The app reconciles selected IDs when its available entities change. Calendar filters the supplied event collection by those IDs; it does not fetch missing records.

### Record Interactions [#record-interactions]

| `eventConfig` field | Signature                                | When to use it                                                    |
| ------------------- | ---------------------------------------- | ----------------------------------------------------------------- |
| `renderPreview`     | `(event: CalendarEvent) => ReactNode`    | Replace default metadata with a domain-specific preview           |
| `onOpen`            | `(event: CalendarEvent) => void`         | Show Open record and route to the full record when it is pressed  |
| `getMenu`           | `(event: CalendarEvent) => DropdownMenu` | Supply recursive actions; available in Agenda and record previews |

An event click opens its preview; it does **not** call `onOpen`. Action callbacks receive the same record object you supplied. Check permissions before supplying action items, and apply the same checks in your API.

### Creation Config [#creation-config]

`addConfig.actions` is a readonly array of `{ id, label, onAction }`. Its context is:

```tsx
type CalendarAddContext = {
  date: string;
  entityIds: readonly string[];
};
```

The first action is the toolbar default; remaining actions appear in its split menu. Every supplied action appears in the day menu. Omit `addConfig` for a review-only calendar. An empty action array also hides creation controls.

### Loading, Errors and Empty Results [#loading-errors-and-empty-results]

| `stateConfig` field | Type                                        | Behavior                                                                          |
| ------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- |
| `isLoading`         | `boolean`                                   | Replaces the event content with a named loading status; navigation remains usable |
| `loadingLabel`      | `string`                                    | Defaults to “Loading events…”                                                     |
| `error`             | `{ message: string; onRetry?: () => void }` | Displays the message and an optional Retry action                                 |
| `emptyContent`      | `ReactNode`                                 | Shown when the selected month/filter has no events                                |

Precedence is **loading → error → ready/empty**. On a request failure, clear loading and provide the error. Use empty content only for a successful request with no matching records. Do not pass stale results for a new visible range.

### Visible Range Callback [#visible-range-callback]

```tsx
type CalendarVisibleRange = {
  start: string; // Inclusive YYYY-MM-DD
  end: string; // Inclusive YYYY-MM-DD
  month: string; // YYYY-MM
  view: 'month' | 'agenda'; // Effective view, never 'auto'
};
```

The callback runs after the initial render and when its range or effective view changes. Changing entities alone does not change the date range; include entity state separately in your request key. `auto` can report Agenda initially and then Month after the container is measured, so request cancellation matters.

Dates are canonical date-only values. No timestamps, locale-formatted strings, UTC-midnight conversions or implicit browser clock. Calendar arithmetic does not shift dates across timezones. The supported range is years 1000–9999; boundary grid slots outside that range are blank. Invalid dates throw a descriptive RangeError; validate API data before rendering.

## Keyboard and Accessibility [#keyboard-and-accessibility]

| Key / input                     | Result                                                                     |
| ------------------------------- | -------------------------------------------------------------------------- |
| Tab / Shift+Tab                 | Move through navigation, filters, event triggers and available day actions |
| Enter / Space on an event       | Open its preview                                                           |
| Enter / Space on N more         | Open all events for the day                                                |
| Escape                          | Close the active popup and restore focus to its trigger                    |
| Arrow keys in the view selector | Choose Month or Agenda                                                     |
| Hover / focus a status mark     | Show Completed or Overdue text                                             |

Provide an accessible `label`, meaningful event titles and text status labels. Color is supplementary. Every event remains reachable through its day overflow. The mobile default is Agenda; an explicitly chosen Month view uses internal horizontal scrolling.

## Figma Rules and Decisions [#figma-rules-and-decisions]

- **CALENDAR-001:** Shared toolbar and filter state for both views; named navigation controls and month announcements.
- **CALENDAR-002:** Outside-month text uses pearl-800 instead of the master’s pearl-600 to meet text contrast. Sunday-first default, five/six rows, 120px nominal days, 4px padding, half-pixel rules.
- **CALENDAR-003:** Three chips plus overflow; selecting from a day list replaces its content in one dialog.
- **CALENDAR-004:** 20px chips, 3px bar, 12px circular flag, single-line title. The preview exposes full text.
- **CALENDAR-005:** Revised neutral fills; separate category and lifecycle presentation.
- **CALENDAR-006:** Status tooltip targets are keyboard-focusable siblings, never nested buttons.
- **CALENDAR-007:** Agenda retains status104 / avatar24 / actions24 slots; missing assignments do not shift columns.
- **CALENDAR-008:** Creation forwards the date and entity IDs; a draft is temporary, not saved data.
- **CALENDAR-009:** Enter/Space open previews. Escape/outside dismiss; focus returns to the originating trigger.
- **CALENDAR-010:** Month changes are instant for all input methods, including reduced motion. The toolbar never slides.

Month uses table semantics and native buttons, not an incomplete ARIA grid. Tab traverses interactive records and add targets; arrow keys operate the view selector and existing selection/menu primitives. Mobile defaults to Agenda. No dragging, timed-event scheduling or recurrence engine is included.

Sources: [Calendar rules](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=804-5708), [component masters](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=781-3), [interaction examples](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=790-1134). Revised category modes were verified directly from Figma, including indigo Transfer pricing and teal Payroll.
