Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6a63eb3
feat(antares): add Calendar and RangeCalendar components
egaitan-godaddy May 29, 2026
605ca15
chore: add changeset for Calendar and RangeCalendar
egaitan-godaddy May 29, 2026
4d17591
refactor(antares): use Flex for Calendar and RangeCalendar layout
egaitan-godaddy May 29, 2026
14be6c7
refactor(antares): simplify Calendar with RAC 1.18 pickers and per-mo…
egaitan-godaddy May 30, 2026
d654f77
fix(antares): pin RangeCalendar header dropdowns to visibleRange.start
egaitan-godaddy May 30, 2026
4fedb80
refactor(antares): replace picker-state Proxy with shallow object spread
egaitan-godaddy May 30, 2026
5cc7161
refactor(antares): use antares Button in CalendarHeader and trim docs
egaitan-godaddy May 30, 2026
d9f239c
refactor(antares): rename CalendarHeader position to range and flip c…
egaitan-godaddy May 30, 2026
9d2d7aa
docs(antares): point Calendar Working-with-dates to DateField
egaitan-godaddy Jun 1, 2026
b74e7ed
docs(antares): rephrase Calendar Working-with-dates as a prose reference
egaitan-godaddy Jun 1, 2026
1cbe7c6
chore(antares): add @internationalized/date dep, drop stray plan SVGs
egaitan-godaddy Jun 4, 2026
85d1482
Merge branch 'main' into feat/calendar
egaitan-godaddy Jun 4, 2026
c12da21
Merge branch 'main' into feat/calendar
rmarkins-godaddy Jun 5, 2026
fd2022c
Merge branch 'main' into feat/calendar
egaitan-godaddy Jun 5, 2026
b2f3b54
Merge branch 'main' into feat/calendar
egaitan-godaddy Jun 17, 2026
825fb0f
refactor(select): streamline Select component and enhance documentation
egaitan-godaddy Jun 17, 2026
2e648d6
Merge branch 'main' into feat/calendar
egaitan-godaddy Jun 17, 2026
f57ca4b
refactor(calendar): consolidate Calendar and RangeCalendar components…
egaitan-godaddy Jun 18, 2026
21a8985
refactor(calendar): replace YearPicker with NumberField for year sele…
egaitan-godaddy Jun 18, 2026
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
5 changes: 5 additions & 0 deletions .changeset/calendar-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@godaddy/antares': minor
---

feat(antares): add Calendar and RangeCalendar components
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ packages/@godaddy/**/test/__screenshots__/**/*-darwin.png

# Claude
.claude/settings.local.json
.claude

# Nx
.nx/cache
Expand All @@ -77,4 +78,4 @@ packages/@godaddy/**/test/__screenshots__/**/*-darwin.png

.nx/polygraph

.claude/worktrees
.claude/worktrees
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 83 additions & 0 deletions packages/@godaddy/antares/components/calendar/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: Calendar
description: Calendar and RangeCalendar are stand-alone date grids for picking a single date or a contiguous range. Use them when you need an always-visible calendar; use DatePicker / DateRangePicker when the calendar should appear in a popover.
---

import { ArgTypes, Meta, Source, Story } from '@storybook/addon-docs/blocks';
import * as Stories from './calendar.stories.tsx';

import SourceDefault from './examples/default.tsx?raw';
import SourceDefaultRange from './examples/default-range.tsx?raw';
import SourceWithMinMax from './examples/with-min-max.tsx?raw';
import SourceWithUnavailableDates from './examples/with-unavailable-dates.tsx?raw';
import SourceTodayDistinct from './examples/today-distinct.tsx?raw';

<Meta of={Stories} name="Overview" />

## Features

- **Single and range**: `Calendar` for a single date, `RangeCalendar` for a contiguous range
- **Built-in Month + Year pickers**: prev/next arrows alongside React Aria's `<CalendarMonthPicker>` / `<CalendarYearPicker>` rendered into the antares `Select`
- **Bounds**: `minValue` and `maxValue` constrain selectable dates and clamp the year `Select`'s range automatically
- **React Aria integration**: built on React Aria Calendar / RangeCalendar for accessibility, keyboard, and locale handling

## Installation

```bash
npm install --save @godaddy/antares
```

## Working with dates

Both components are typed for `CalendarDate` from `@internationalized/date`. See the
`DateField` component docs for installation, locale handling, and common patterns.

## Props

### Calendar

<ArgTypes of={Stories.Props} />

### RangeCalendar

<ArgTypes of={Stories.RangeProps} />

## Examples

### Default

A single calendar with one month visible.

<Source language="tsx" code={SourceDefault} />
<Story of={Stories.Default} inline />

### Default range

`RangeCalendar` shows two months side-by-side. Each visible month has its own header
(prev arrow + dropdowns on the left, dropdowns + next arrow on the right). Picking a
month or year on either side scrolls both grids together; the prev/next arrows page by
one month.

<Source language="tsx" code={SourceDefaultRange} />
<Story of={Stories.DefaultRange} inline />

### Min and max

`minValue` and `maxValue` disable out-of-range days and clamp the year `Select`.

<Source language="tsx" code={SourceWithMinMax} />
<Story of={Stories.WithMinMax} inline />

### Unavailable dates

Use `isDateUnavailable` to disable specific dates such as weekends or holidays.

<Source language="tsx" code={SourceWithUnavailableDates} />
<Story of={Stories.WithUnavailableDates} inline />

### Today inside a range

The "today" visual state coexists with `selected` and `in-range`.

<Source language="tsx" code={SourceTodayDistinct} />
<Story of={Stories.TodayDistinct} inline />
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';
import { getComponentDocs, getMeta, getStory } from '@bento/storybook-addon-helpers';
import { CalendarDefaultExample } from './examples/default.tsx';
import { CalendarDefaultRangeExample } from './examples/default-range.tsx';
import { CalendarTodayDistinctExample } from './examples/today-distinct.tsx';
import { CalendarWithMinMaxExample } from './examples/with-min-max.tsx';
import { CalendarWithUnavailableDatesExample } from './examples/with-unavailable-dates.tsx';
import { Calendar, RangeCalendar } from './src/index.tsx';

export default getMeta({
title: 'components/Calendar'
});

export const Props = getComponentDocs(Calendar);
export const RangeProps = getComponentDocs(RangeCalendar);

export const Default = getStory(CalendarDefaultExample);

export const DefaultRange = getStory(CalendarDefaultRangeExample);

export const WithMinMax = getStory(CalendarWithMinMaxExample);

export const WithUnavailableDates = getStory(CalendarWithUnavailableDatesExample);

export const TodayDistinct = getStory(CalendarTodayDistinctExample);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { parseDate } from '@internationalized/date';
import { RangeCalendar } from '@godaddy/antares';

export function CalendarDefaultRangeExample() {
return (
<RangeCalendar
aria-label="Booking range"
defaultValue={{
start: parseDate('2024-03-05'),
end: parseDate('2024-03-12')
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { parseDate } from '@internationalized/date';
import { Calendar } from '@godaddy/antares';

export function CalendarDefaultExample() {
return <Calendar aria-label="Date" defaultValue={parseDate('2024-03-15')} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getLocalTimeZone, today } from '@internationalized/date';
import { RangeCalendar } from '@godaddy/antares';

export function CalendarTodayDistinctExample() {
const now = today(getLocalTimeZone());
return (
<RangeCalendar
aria-label="Today inside selected range"
defaultValue={{ start: now.subtract({ days: 3 }), end: now.add({ days: 3 }) }}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { parseDate } from '@internationalized/date';
import { Calendar } from '@godaddy/antares';

export function CalendarWithMinMaxExample() {
return (
<Calendar
aria-label="Q1 2024"
defaultValue={parseDate('2024-02-15')}
minValue={parseDate('2024-01-01')}
maxValue={parseDate('2024-03-31')}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type DateValue, parseDate } from '@internationalized/date';
import { Calendar } from '@godaddy/antares';

export function CalendarWithUnavailableDatesExample() {
function isWeekend(date: DateValue) {
const dayOfWeek = date.toDate('UTC').getUTCDay();
return dayOfWeek === 0 || dayOfWeek === 6;
}

return (
<Calendar
aria-label="Weekday only"
defaultValue={parseDate('2024-03-13')}
isDateUnavailable={isWeekend}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { CalendarDate } from '@internationalized/date';
import { useContext } from 'react';
import {
CalendarMonthPicker as RACCalendarMonthPicker,
CalendarStateContext,
CalendarYearPicker as RACCalendarYearPicker,
RangeCalendarStateContext,
useLocale
} from 'react-aria-components';
import { Button } from '#components/button';
import { Flex } from '#components/layout/flex';
import { Icon } from '#components/icon';
import { Select, SelectItem } from '#components/select';
import styles from './index.module.css';

interface CalendarHeaderProps {
/**
* `'start'` / `'end'` for the two grids of `RangeCalendar`. Omit for the single
* grid of `Calendar`. Names match the range value (`{ start, end }`) and are
* locale-direction-agnostic — in RTL the start grid renders on the right.
*/
range?: 'start' | 'end';
}

/**
* Header with prev/next + Month/Year selects, powered by RAC's `<CalendarMonthPicker>` /
* `<CalendarYearPicker>` rendered through the antares `Select`.
*
* RAC's pickers read state from context and key off `focusedDate`. We pin `focusedDate`
* to `visibleRange.start` (plus a one-month offset for `range='end'`) and translate
* `setFocusedDate` back, so the dropdowns track the visible month and selecting on
* either side scrolls both grids together.
*
* Chevron icons flip in RTL so "previous" always points opposite the reading direction.
*/
export function CalendarHeader({ range }: CalendarHeaderProps) {
const calendarState = useContext(CalendarStateContext);
const rangeState = useContext(RangeCalendarStateContext);
const baseState = calendarState ?? rangeState;
const { direction } = useLocale();
const monthOffset = range === 'end' ? 1 : 0;

if (!baseState) return null;

const pickerState = {
...baseState,
focusedDate: baseState.visibleRange.start.add({ months: monthOffset }),
setFocusedDate(date: CalendarDate) {
baseState.setFocusedDate(date.subtract({ months: monthOffset }));
}
};

const showPrev = range !== 'end';
const showNext = range !== 'start';
const isRtl = direction === 'rtl';

const headerContent = (
<Flex gap="sm" alignItems="center" className={styles.header}>
{showPrev && (
<Button slot="previous" variant="minimal" size="sm" aria-label="Previous">
<Icon icon={isRtl ? 'chevron-right' : 'chevron-left'} />
</Button>
)}
<RACCalendarMonthPicker format="long">
{function renderMonthPicker(picker) {
return (
<Select
aria-label={picker['aria-label']}
value={picker.value}
onChange={picker.onChange}
className={styles.headerSelect}
>
{picker.items.map(function renderMonthItem(item) {
return (
<SelectItem key={item.id} id={item.id} textValue={item.formatted}>
{item.formatted}
</SelectItem>
);
})}
</Select>
);
}}
</RACCalendarMonthPicker>
<RACCalendarYearPicker>
{function renderYearPicker(picker) {
return (
<Select
aria-label={picker['aria-label']}
value={picker.value}
onChange={picker.onChange}
className={styles.headerSelect}
>
{picker.items.map(function renderYearItem(item) {
return (
<SelectItem key={item.id} id={item.id} textValue={item.formatted}>
{item.formatted}
</SelectItem>
);
})}
</Select>
);
}}
</RACCalendarYearPicker>
{showNext && (
<Button slot="next" variant="minimal" size="sm" aria-label="Next">
<Icon icon={isRtl ? 'chevron-left' : 'chevron-right'} />
</Button>
)}
</Flex>
);

if (calendarState) {
return (
<CalendarStateContext.Provider value={pickerState as typeof calendarState}>
{headerContent}
</CalendarStateContext.Provider>
);
}
return (
<RangeCalendarStateContext.Provider value={pickerState as typeof rangeState}>
{headerContent}
</RangeCalendarStateContext.Provider>
);
}
Loading
Loading