Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f86a33b
feat(antares): add Calendar component
egaitan-godaddy Jun 24, 2026
b3fae2f
test(antares): add Calendar value and RangeCalendar examples
egaitan-godaddy Jun 24, 2026
7461725
test(antares): Calendar interaction and visual coverage
egaitan-godaddy Jun 24, 2026
1612be0
feat(antares): add DatePicker component
egaitan-godaddy Jun 24, 2026
78bad0a
feat(antares): add DateRangePicker component
egaitan-godaddy Jun 24, 2026
8ce9007
test(antares): DatePicker interaction and visual coverage
egaitan-godaddy Jun 24, 2026
dbc879a
docs(antares): Calendar and DatePicker stories and READMEs
egaitan-godaddy Jun 24, 2026
d73061e
Merge branch 'main' into feat/date-field-components
egaitan-godaddy Jun 25, 2026
d4c9a0a
Merge branch 'main' into feat/date-field-components
egaitan-godaddy Jun 25, 2026
1f4ff0f
docs(antares): Calendar and DatePicker stories and READMEs
egaitan-godaddy Jun 24, 2026
4c9e4a4
chore: addressing styles
egaitan-godaddy Jul 3, 2026
43a8a43
docs(calendar): multi-month Calendar/RangeCalendar design spec
egaitan-godaddy Jul 3, 2026
4c226bd
feat(calendar): render multiple visible months with per-month header
egaitan-godaddy Jul 3, 2026
e3525d8
docs(calendar): add month-count control to playground
egaitan-godaddy Jul 3, 2026
c649c82
refactor(calendar): pageCount prop with grid-aligned multi-month nav
egaitan-godaddy Jul 3, 2026
3a05be8
docs(antares-components): allow pinned generics in polymorphic as
egaitan-godaddy Jul 3, 2026
5daac34
docs(calendar): document props and remaining examples
egaitan-godaddy Jul 3, 2026
560b274
docs(date-picker): add field-trigger sizing spec
egaitan-godaddy Jul 3, 2026
4be2fa2
docs(date-picker): add field-trigger implementation plan
egaitan-godaddy Jul 3, 2026
b4dce95
refactor(field): extract FieldTrigger and decouple chevron rotation
egaitan-godaddy Jul 3, 2026
0e291c8
test(date-picker): assert short-month trigger label after selection
egaitan-godaddy Jul 3, 2026
90b8cf1
fix(date-picker): size trigger via shared FieldTrigger
egaitan-godaddy Jul 3, 2026
9bcdba1
chore: removing unwanted files
egaitan-godaddy Jul 3, 2026
2ba9334
Merge branch 'main' into feat/date-field-components
egaitan-godaddy Jul 3, 2026
47420b7
test: update visual regression screenshots
github-actions[bot] Jul 3, 2026
be42d1c
chore: adding calendar icon to tests
egaitan-godaddy Jul 4, 2026
f413643
chore: jsdoc doc fixes
egaitan-godaddy Jul 6, 2026
fdb283d
chore: docs minor formatting tweaks
egaitan-godaddy Jul 6, 2026
024f498
chore: guarding against null keys for month picker
egaitan-godaddy Jul 6, 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
2 changes: 1 addition & 1 deletion .agents/skills/antares-components/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Scoped conventions for component work in `packages/@godaddy/antares/`.
- Prefer `Flex`, `Box`, and `Grid` over hand-rolled CSS. Use layout props (`direction`, `gap`, `alignItems`, `justifyContent`, `wrap`, `padding`, `inlinePadding`, `blockPadding`, `display`) before adding flex/gap rules in `*.module.css`.
- Use `Flex as={X}` (or `Box as=…`) to give an element flex/layout semantics instead of an extra wrapper div — works for RAC primitives (`RACButton`, …) and native tags (`"div"`, `"nav"`, `"ol"`, `"li"`, etc).
- When a component wraps such an element, extend `Omit<FlexOwnProps, 'as'>` and spread the rest onto it so callers can pass layout/HTML props. Put fixed props after the `{...rest}` spread so they can't be overridden.
- Polymorphic `as` does **not** support TypeScript generics — see `types/polymorphic-react.ts`. For generic RAC components (e.g. `RACCalendar<CalendarDate>`, `RACSelect<T>`, `RACListBox<T>`), keep the generic component as the outer element and nest a `Flex` inside rather than `Flex as={RACCalendar}`.
- Polymorphic `as` can't infer a generic RAC component's type params (see `types/polymorphic-react.ts`). Either pin them at the call site — `Flex as={RACCalendar<CalendarDate>}` — or keep the generic component as the outer element with a `Flex` nested inside. Never use a bare, unparameterized `Flex as={RACCalendar}`.
- Spacing tokens: `gap="sm"`, `gap="md"`, etc. In CSS, use `var(--sp-sm)` directly. Tokens defined in `components/layout/tokens.ts`. Use t-shirt sizes (`sm`, `md`, `lg`).

## Imports
Expand Down
14 changes: 14 additions & 0 deletions .changeset/date-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@godaddy/antares': minor
---

feat(antares): add Calendar, RangeCalendar, DatePicker, DateRangePicker and FieldTrigger

Adds date components built on React Aria Components:

- `Calendar` / `RangeCalendar` — accessible single-date and date-range calendars. The header
pairs a month `Select` with a typeable year `NumberField`, flanked by prev/next navigation.
- `DatePicker` / `DateRangePicker` — date-only (`CalendarDate`) fields that render the selected
value as a read-only formatted label and open a calendar in a popover (no editable segmented
input). Built on the field primitives, with `size`, `formatOptions`, and `placeholder` props.
- `FieldTrigger` — Used in DatePicker and Select as the component sharing their styles.
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.

111 changes: 111 additions & 0 deletions packages/@godaddy/antares/components/calendar/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: Calendar
description: Accessible calendar grids for selecting a single date (Calendar) or a date range (RangeCalendar), built on React Aria.
---

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 SourceWithValue from './examples/with-value.tsx?raw';
import SourceRange from './examples/range.tsx?raw';
import SourceMinMax from './examples/min-max.tsx?raw';
import SourceUnavailable from './examples/unavailable.tsx?raw';
import SourceDisabled from './examples/disabled.tsx?raw';

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

## Features

- Single-date (`Calendar`) and range (`RangeCalendar`) grids built on React Aria
- Month dropdown plus a typeable year input in the header for fast navigation
- Keyboard navigable and screen-reader announced
- `minValue` / `maxValue` bounds and `isDateUnavailable` per-day disabling
- Date-only values via `@internationalized/date`'s `CalendarDate`

## Installation

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

## Working with dates

`Calendar` values are `CalendarDate` objects from `@internationalized/date`, which is included transitively with `@godaddy/antares`. Add `@internationalized/date` to your `package.json` and use it directly:

```tsx
import { parseDate, today, getLocalTimeZone } from '@internationalized/date';
import { Calendar } from '@godaddy/antares';

<Calendar aria-label="Date" defaultValue={parseDate('2024-03-15')} />;
<Calendar aria-label="Today" defaultValue={today(getLocalTimeZone())} />;
```

## Props

The `Calendar` component accepts the following props:

<ArgTypes of={Stories.Props} />

The `RangeCalendar` component accepts the following props:

<ArgTypes of={Stories.RangeCalendarProps} />

## Examples

### Basic

A single-date calendar.

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

### With value

A calendar with a pre-selected date.

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

### Range

Selecting a start and end date.

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

### Min / Max

Bounding selectable dates.

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

### Unavailable

Disabling scattered individual dates with `isDateUnavailable`.

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

### Disabled

A fully disabled calendar.

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

## Accessibility

- Arrow keys move by day; PageUp/PageDown by month; Home/End to row edges
- Focus-visible outline; disabled days are not focusable
- Out-of-month days render at reduced opacity and are not selectable

## Best Practices

- Always provide an `aria-label` on standalone calendars.
- Use `isDateUnavailable` for scattered disabled days; use `minValue` / `maxValue` for contiguous bounds.

## Troubleshooting

- Dates not formatting per locale? Ensure the app is wrapped in `<I18nProvider locale="…">`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';
import { getComponentDocs, getMeta, getStory } from '@bento/storybook-addon-helpers';
import { Calendar, RangeCalendar } from '@godaddy/antares';
import { PlaygroundExample, type PlaygroundExampleProps } from './examples/calendar-playground.tsx';
import { CalendarDefaultExample } from './examples/default.tsx';
import { CalendarWithValueExample } from './examples/with-value.tsx';
import { RangeCalendarExample } from './examples/range.tsx';
import { CalendarMinMaxExample } from './examples/min-max.tsx';
import { CalendarUnavailableExample } from './examples/unavailable.tsx';
import { CalendarDisabledExample } from './examples/disabled.tsx';

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

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

export const Playground = {
render: (args: PlaygroundExampleProps) => <PlaygroundExample {...args} />,
args: { isDisabled: false, isReadOnly: false, pageCount: 1 },
argTypes: {
isDisabled: { control: 'boolean', description: 'Disables the calendar.' },
isReadOnly: { control: 'boolean', description: 'Makes the calendar read-only.' },
pageCount: {
control: 'radio',
options: [1, 2, 3],
description: 'Number of month grids to display (maps to visibleDuration).'
}
}
};

export const Default = getStory(CalendarDefaultExample);
export const WithValue = getStory(CalendarWithValueExample);
export const Range = getStory(RangeCalendarExample);
export const MinMax = getStory(CalendarMinMaxExample);
export const Unavailable = getStory(CalendarUnavailableExample);
export const Disabled = getStory(CalendarDisabledExample);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Calendar } from '@godaddy/antares';

export interface PlaygroundExampleProps {
isDisabled?: boolean;
isReadOnly?: boolean;
/** Number of month grids to display (maps to `visibleDuration`). */
pageCount?: number;
}

export function PlaygroundExample(props: PlaygroundExampleProps) {
const { pageCount = 1, ...rest } = props;
return <Calendar aria-label="Calendar" {...rest} pageCount={pageCount} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Calendar } from '@godaddy/antares';
import { parseDate } from '@internationalized/date';

// Unselected calendar pinned to a fixed visible month so the month/year header snapshots
// deterministically — without a fixed focus it would show today's month and churn daily.
export function CalendarDefaultExample() {
return <Calendar aria-label="Event date" defaultFocusedValue={parseDate('2024-03-01')} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Calendar } from '@godaddy/antares';
import { parseDate } from '@internationalized/date';

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

export function CalendarMinMaxExample() {
return (
<Calendar
aria-label="Booking date"
defaultValue={parseDate('2024-03-15')}
minValue={parseDate('2024-03-05')}
maxValue={parseDate('2024-03-25')}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { RangeCalendar } from '@godaddy/antares';
import { parseDate } from '@internationalized/date';

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

export function CalendarUnavailableExample() {
return (
<Calendar
aria-label="Weekday only"
defaultValue={parseDate('2024-03-15')}
isDateUnavailable={(date) => isWeekend(date, 'en-US')}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Calendar } from '@godaddy/antares';
import { parseDate } from '@internationalized/date';

export function CalendarWithValueExample() {
return <Calendar aria-label="Event date" defaultValue={parseDate('2024-03-15')} />;
}
105 changes: 105 additions & 0 deletions packages/@godaddy/antares/components/calendar/src/CalendarHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Icon } from '#components/icon';
import { Flex } from '#components/layout/flex';
import { NumberField } from '#components/number-field';
import { Select, SelectItem } from '#components/select';
import { Button } from '#components/button';
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date';
import { useCallback, useContext, useMemo } from 'react';
import {
CalendarStateContext as RACCalendarStateContext,
RangeCalendarStateContext as RACRangeCalendarStateContext,
useLocale,
type Key as RACKey
} from 'react-aria-components';
import styles from './index.module.css';

/**
* Previous/next navigation arrow (RAC `slot`). `hidden` renders an inert, space-retaining
* placeholder so inward-facing arrows in a multi-month layout keep the controls centered.
*/
export function NavButton(props: { direction: 'previous' | 'next'; hidden?: boolean }) {
const { direction, hidden = false } = props;
// Slot is always set (RAC requires it); navHidden's visibility:hidden makes the placeholder inert.
return (
<Button
slot={direction}
aria-label={direction === 'previous' ? 'Previous' : 'Next'}
className={hidden ? styles.navHidden : undefined}
>
<Icon icon={direction === 'previous' ? 'chevron-left' : 'chevron-right'} />
</Button>
);
}

/**
* Editable month/year controls for the month `offset` months into the visible range. Subtracting
* `offset` on change keeps the edit anchored to the first visible month.
*/
export function MonthHeading(props: { offset: number }) {
const { offset } = props;
const calendarState = useContext(RACCalendarStateContext);
const rangeState = useContext(RACRangeCalendarStateContext);
const state = calendarState ?? rangeState;
const { locale } = useLocale();
const displayDate = state?.visibleRange.start.add({ months: offset }) ?? null;

// Localized month names for the dropdown; recompute only when locale or the shown year changes.
const monthNames = useMemo(
function computeMonthNames() {
const formatter = new DateFormatter(locale, { month: 'long' });
const year = displayDate?.year ?? 2024;
return Array.from({ length: 12 }, (_, index) =>
formatter.format(new CalendarDate(year, index + 1, 1).toDate(getLocalTimeZone()))
);
},
[locale, displayDate?.year]
);

const handleMonthChange = useCallback(
function handleMonthChange(key: RACKey | null) {
if (!state || !displayDate || key === null) {
return;
}

state.setFocusedDate(displayDate.set({ month: Number(key) }).subtract({ months: offset }));
},
[state, displayDate, offset]
);

const handleYearChange = useCallback(
function handleYearChange(year: number) {
if (!state || !displayDate || Number.isNaN(year)) {
return;
}

state.setFocusedDate(displayDate.set({ year }).subtract({ months: offset }));
},
[state, displayDate, offset]
);

if (!state || !displayDate) {
return null;
}

return (
<Flex direction="row" gap="sm" alignItems="center">
<Select aria-label="Month" value={String(displayDate.month)} onChange={handleMonthChange}>
{monthNames.map(function mapMonthNames(name, index) {
return (
<SelectItem key={index + 1} id={String(index + 1)}>
{name}
</SelectItem>
);
})}
</Select>
<NumberField
aria-label="Year"
hideStepper
formatOptions={{ useGrouping: false }}
value={displayDate.year}
onChange={handleYearChange}
className={styles.yearField}
/>
</Flex>
);
}
Loading