Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
18abab4
feat(core): add day-range and day-shift drag helpers
afonsojramos Aug 20, 2026
5688dd6
test: record pan callbacks in the gesture-handler mock
afonsojramos Aug 20, 2026
1d36e7a
feat(dom): support selecting, creating and moving events on the month…
afonsojramos Aug 20, 2026
ad62176
feat(dom): add date selection and drag-to-create to the year grid
afonsojramos Aug 20, 2026
821960c
feat(native): support selecting, creating and moving events on the mo…
afonsojramos Aug 20, 2026
30350a8
feat(native): add date selection and drag-to-create to the year grid
afonsojramos Aug 20, 2026
be9d056
feat(native): forward selection and drag handlers through the month p…
afonsojramos Aug 20, 2026
6136bfb
feat: wire month and year selection and drag handlers through Calendar
afonsojramos Aug 20, 2026
d325217
test: drop the month onCreateEvent platform exemption
afonsojramos Aug 20, 2026
40875a4
docs: document month and year selection, creation and drag
afonsojramos Aug 20, 2026
a6ef21f
Merge origin/main into issue-41
afonsojramos Aug 20, 2026
c61d11c
fix(dom): harden the year grid's drag-to-select
afonsojramos Aug 20, 2026
dd4cc43
fix(native): harden the year grid's drag-to-select
afonsojramos Aug 20, 2026
b3215ac
fix(dom): keep disabled days out of a month sweep and drop
afonsojramos Aug 20, 2026
a0f6943
fix(native): repaint cached month pages when the sweep props change
afonsojramos Aug 20, 2026
d71a56a
docs: drop em-dashes and correct the year-mode selection props
afonsojramos Aug 20, 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
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,20 @@ dragged so the **time of day and duration are preserved**. Grab with a
(`draggable`, `startEditable`, `eventStartEditable`, `eventOverlap`, and
returning `false` to reject); there is no resize on the month grid.

**On the year grid.** The mini months take the same sweep: `onSelectDrag` reports
the ordered inclusive `[start, end]` days as you drag (pair it with
`useDateRange`'s `selectRange`), and `onCreateEvent` reports the all-day range
once on release. `onSelectDrag` works on the month grid too, so a selection
highlight can follow a sweep instead of appearing only on release. The year view
shows dots rather than bars, so it has nothing to pick up: `onDragEvent` is
month-mode only.

```tsx
<Calendar
mode="month"
/* ... */
selectedRange={range ?? undefined}
onSelectDrag={selectRange}
onCreateEvent={(start, end) =>
setEvents((prev) => [...prev, { id: makeId(), title: "New", start, end, allDay: true }])
}
Expand Down Expand Up @@ -413,11 +423,11 @@ duration and fields; non-recurring events pass through unchanged.

### Date selection

Date picking lives on `MonthList`, the vertically-scrolling month list (the
horizontally-paged `month` view is for browsing events, not picking). A range's
Date picking lives on `MonthList`, the vertically-scrolling month list. A range's
endpoints get a filled badge (the `selectedBackground` token) and the span gets
a centered rounded "pill" band behind it; today keeps its own badge. For ranges,
the `useDateRange` hook
a centered rounded "pill" band behind it; today keeps its own badge. `Calendar`
takes the same props in `month` and `year` modes, so an events calendar can carry
a selection too. For ranges, the `useDateRange` hook
owns the state machine: the first press sets the start, the second sets the end
(auto-swapping if earlier), a third press starts over. Tap two days, or
long-press and drag to sweep a range (the list auto-scrolls at the edges, so a
Expand Down Expand Up @@ -448,6 +458,12 @@ and height are the `rangeBackground` / `rangeBandHeight` theme tokens; pass
`fillCellOnSelection` to `MonthList` to fill the whole cell edge to edge instead
of the pill.

**In the calendar.** `Calendar` accepts `selectedDates`, `selectedRange`,
`minDate`, `maxDate`, `isDateDisabled`, and `onSelectDrag` in `month` and `year`
modes, so the same model works on a grid that also shows events
(`fillCellOnSelection` applies to the month grid only). On the month grid a sweep starts from empty day space (dragging a chip
moves the event instead); on the year grid any day starts one.

**Disabled days.** `minDate`, `maxDate` and `isDateDisabled` render days dimmed,
ignore taps, and keep them out of any selection (drag included). Hand the same
constraints to `useDateRange` so a blocked day never opens a range:
Expand Down
35 changes: 32 additions & 3 deletions docs/guides/dragging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ description: Move, resize, reschedule across days, and create events by dragging
Everything here is opt-in: pass the relevant handler and update your own event
state in response. Move, resize, and create work on the week/day grid; move and
create also work on the month grid (see [On the month grid](#on-the-month-grid)),
where a drag moves an event by whole days rather than by time.
where a drag moves an event by whole days rather than by time. The year grid
sweeps out day ranges too (see [On the year grid](#on-the-year-grid)).

## Move and resize

Expand Down Expand Up @@ -199,6 +200,11 @@ click without dragging creates nothing and still fires `onPressDay`.
/>
```

`onSelectDrag` reports the **same sweep** as it happens, with the ordered inclusive
`[start, end]` days, so a selection highlight can follow the drag rather than
appearing only on release. Set it, `onCreateEvent`, or both; either enables the
gesture. See [Date selection](/guides/selection) for wiring it to `useDateRange`.

### Drag to reschedule

Pick an event bar up and drop it on another day. Both ends shift by the same
Expand Down Expand Up @@ -232,7 +238,30 @@ about to land on carries `data-drop`, so you can restyle either (see
[Styling](/guides/styling)); give the `day` slot a class to take the tint over
entirely.

## On the year grid

The twelve mini months take the same sweep: `onSelectDrag` reports the ordered
inclusive `[start, end]` days as you drag, and `onCreateEvent` reports the
all-day range once on release. Hold a day and drag on a device, press and drag on
the web. Days being swept carry `data-creating` on the dom renderer.

```tsx
<Calendar
mode="year"
/* ... */
selectedRange={range ?? undefined}
onSelectDrag={selectRange}
onCreateEvent={(start, end) =>
setEvents((prev) => [...prev, { id: makeId(), title: "Leave", start, end, allDay: true }])
}
/>
```

On a device a sweep stays inside the mini month it started in; on the web it can
carry on across months. The year view summarises events as dots rather than bars,
so it has nothing to pick up: `onDragEvent` applies to `month` mode, not `year`.

## Tuning the snap

`dragStepMinutes` (default 15) controls how move, resize, and create snap. Set it
to `5`, `30`, etc. to match your grid's granularity.
`dragStepMinutes` (default 15) controls how move, resize, and create snap on the
week/day grid. The month and year grids always snap to whole days.
24 changes: 24 additions & 0 deletions docs/guides/month-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,29 @@ all-day span across days, and `onDragEvent` to drop an event on another day
(keeping its time of day and duration). See
[Drag & create](/guides/dragging#on-the-month-grid).

## Selecting, creating, and moving by drag

The month grid is drag-driven too, in whole days. **Hold an empty day and drag**
across others (press and drag on the web) to sweep a span: `onSelectDrag` reports
it live as the ordered `[start, end]` days, `onCreateEvent` reports it once on
release as an all-day range. **Hold an event chip** and drop it on another day and
`onDragEvent` fires with the event shifted by whole days, keeping its time of day
and duration. `selectedDates` / `selectedRange` (with `minDate`, `maxDate`,
`isDateDisabled`) draw the selection.

```tsx
<Calendar
mode="month"
/* ... */
selectedRange={range ?? undefined}
onSelectDrag={selectRange}
onCreateEvent={createAllDay}
onDragEvent={moveEvent}
/>
```

See [Drag & create](/guides/dragging) and
[Date selection](/guides/selection) for the details.

Looking for a vertically scrolling list of months instead of a pager, or an
events-free month grid for picking dates? That's [MonthList](/guides/month-list).
34 changes: 34 additions & 0 deletions docs/guides/selection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ month list. It takes the selection props and renders a range as filled endpoint
badges with a centered rounded "pill" band across the span; today keeps its own
badge. Pass `fillCellOnSelection` to fill the whole cell instead of the pill.

`Calendar` takes the same props in `month` and `year` modes, so an events
calendar can carry a selection too. See [In the calendar](#in-the-calendar).

## Range selection with useDateRange

`useDateRange` owns the start/end state machine: the first press sets the start,
Expand Down Expand Up @@ -79,3 +82,34 @@ const { range, onPressDate, selectRange } = useDateRange({ minDate });
Selection is a `rangeBackground` band over the span; there is no per-day selected circle by
default, so the "today" badge stays distinct. See [Theming](/guides/theming) for the token.
</Note>

## In the calendar

`Calendar` accepts `selectedDates`, `selectedRange`, `minDate`, `maxDate`,
`isDateDisabled`, and `onSelectDrag` in `month` and `year` modes, drawn exactly
as they are in the picker (`fillCellOnSelection` applies to the month grid only). So the same selection model works
on top of a calendar that also shows events: pick a range to filter a report,
block out a holiday, or choose the days a new event should span.

```tsx
const { range, onPressDate, selectRange } = useDateRange();

<Calendar
mode="month"
date={date}
events={events}
onChangeDate={setDate}
selectedRange={range ?? undefined}
onPressDay={onPressDate}
onSelectDrag={selectRange}
onPressEvent={openEvent}
/>;
```

Tap two days, or **hold a day and drag** across others (press and drag on the web)
to sweep the range out in one gesture. On the month grid the drag starts from empty
day space, so dragging an event chip still moves the event
(see [Drag & create](/guides/dragging)); on the year grid any day starts a sweep.

Add `onCreateEvent` alongside `onSelectDrag` and the same sweep also reports an
all-day range on release, which is how you turn a swept selection into an event.
20 changes: 20 additions & 0 deletions docs/guides/styling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ variant works on either slot (e.g. `dayBadge: "data-[today]:bg-blue-600"`).
| `data-disabled` | day / badge | the day is disabled (`min`/`maxDate`) |
| `data-creating` | day / badge | the day is inside a drag-to-create span |
| `data-drop` | day / badge | a dragged event would land on this day |
| `data-events` | year day | the day holds at least one event |
| `data-dragging` | time-grid event | the event is being dragged |

Every slotted element also carries a stable `data-slot="<name>"` attribute, handy for
Expand Down Expand Up @@ -207,6 +208,25 @@ Native's `Agenda` supports `dayHeader`, `eventRow`, and `empty`.
| `event` | The default event card |
| `empty` | The "No events" placeholder |

### YearView

Both renderers support `grid`, `month`, `monthTitle`, `weekdays`, `weekday`,
`week`, `day`, `dayBadge`, `rangeBand`, and `eventDot` (plus `dayText` on React
Native).

| Slot | Element |
| ------------ | ------------------------------------- |
| `grid` | The twelve-month container |
| `month` | Each mini month |
| `monthTitle` | A mini month's title |
| `weekdays` | A mini month's weekday-initials row |
| `weekday` | Each weekday initial |
| `week` | Each week row of a mini month |
| `day` | Each day cell |
| `dayBadge` | The round today/selected highlight |
| `rangeBand` | The selection band layer behind a day |
| `eventDot` | The dot under a day that holds events |

### MonthList (scrolling picker)

`weekdays` / `weekday` style the shared header; every `MonthView` slot above is
Expand Down
67 changes: 63 additions & 4 deletions docs/guides/year-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,57 @@ at least one event gets a small dot (recurring rules are expanded across the
whole year first). Today gets the theme's filled badge; pass `activeDate` to
highlight a different day.

## Selecting dates

The mini months take the same selection props as the month grid and the picker:
`selectedDates`, `selectedRange`, `minDate`, `maxDate`, and `isDateDisabled`.
Selected days and range endpoints get the filled badge (today still wins where
they coincide), the days between get the range band, and disabled days are dimmed
and ignore taps.

`onSelectDrag` adds drag-to-select: **hold a day and drag** across others (press
and drag on the web) and it fires live with the ordered inclusive `[start, end]`,
so the highlight follows the gesture. Pair it with `useDateRange`:

```tsx
const { range, onPressDate, selectRange } = useDateRange();

<Calendar
mode="year"
date={date}
events={events}
onChangeDate={setDate}
selectedRange={range ?? undefined}
onPressDay={onPressDate}
onSelectDrag={selectRange}
/>;
```

On a device a sweep runs across the days of the mini month it started in; on the
web it can carry on across mini months. See [Date selection](/guides/selection)
for the full selection model.

## Creating events

`onCreateEvent` reports the same sweep once, on release, as an **all-day** range:
`start` at midnight of the first day and `end` at midnight after the last
(exclusive). A plain tap still fires `onPressDay`, so drill-down and creation can
coexist.

```tsx
<Calendar
mode="year"
/* ... */
onCreateEvent={(start, end) =>
setEvents((prev) => [...prev, { id: makeId(), title: "Leave", start, end, allDay: true }])
}
/>
```

Because the year view summarises events as dots rather than chips, there is
nothing to pick up: `onDragEvent` (drag an event to another day) applies to
`month` mode, not `year`. See [Drag & create](/guides/dragging).

## Layout

The grid fits as many columns as the width allows, so a phone gets 2 columns
Expand All @@ -64,7 +115,15 @@ import { YearView } from "@super-calendar/native";
## Styling

Every part is a [slot](/guides/styling): `grid`, `month`, `monthTitle`,
`weekdays`, `weekday`, `week`, `day`, and `eventDot` (plus `dayText` on React
Native, where text colour doesn't inherit). On the web, day cells carry
`data-today` and `data-events` for Tailwind variants. The theme's
`todayBackground` / `todayText` drive the badge and dots.
`weekdays`, `weekday`, `week`, `day`, `dayBadge`, `rangeBand`, and `eventDot`
(plus `dayText` on React Native, where text colour doesn't inherit). On the web,
day cells carry `data-today`, `data-events`, `data-selected`, `data-range`,
`data-disabled`, and `data-creating` for Tailwind variants. The theme's
`todayBackground` / `todayText` drive the badge and dots, and `rangeBackground`
the selection band.

<Note>
The today / selected circle now lives on the `dayBadge` slot, not `day` (which is the cell around
it), matching the month grid. If you were restyling that circle through `classNames.day` or
`styles.day`, move those overrides to `dayBadge`.
</Note>
19 changes: 14 additions & 5 deletions docs/reference/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ your editor lists the full set with inline docs.
| ----------------------- | ---------------------------------------- | --------------------------------------------------------------------------------- |
| `onDragEvent` | `(event, start, end) => void \| boolean` | Enable move/resize; return `false` to reject. Month: drop on another day. |
| `onDragStart` | `(event) => void` | Fires on grab (e.g. haptics). |
| `onCreateEvent` | `(start, end) => void` | Drag empty space to create. Month: an all-day span. |
| `onCreateEvent` | `(start, end) => void` | Drag empty space to create. Month and year: an all-day span. |
| `onSelectDrag` | `(start, end) => void` | Month and year: drag to sweep a date range; fires live. |
| `dragStepMinutes` | `number` | Snap step (default 15). |
| `showDragHandle` | `boolean` | Show the resize grip (default true). |
| `eventStartEditable` | `boolean` | Grid-wide: allow moving events (default true). Per-event `startEditable`. |
Expand All @@ -67,10 +68,18 @@ your editor lists the full set with inline docs.

## Selection

Date selection lives on [`MonthList`](/guides/month-list), not the paged `month`
view. It takes `selectedDates` / `selectedRange` (+ `onSelectDrag` for drag),
and `minDate` / `maxDate` / `isDateDisabled` for disabled days. See the
[Date selection](/guides/selection) guide.
| Prop | Type | Notes |
| --------------------- | ---------------------- | ------------------------------------------------ |
| `selectedDates` | `Date[]` | Discrete selected days (month + year). |
| `selectedRange` | `DateRange` | A selected span (month + year). |
| `fillCellOnSelection` | `boolean` | Month: fill the cell instead of the pill band. |
| `minDate` | `Date` | Earliest selectable day (inclusive). |
| `maxDate` | `Date` | Latest selectable day (inclusive). |
| `isDateDisabled` | `(date) => boolean` | Return true to forbid a specific day. |
| `onSelectDrag` | `(start, end) => void` | Drag to sweep a range; pair with `useDateRange`. |

The same props drive [`MonthList`](/guides/month-list), the scrolling picker. See
the [Date selection](/guides/selection) guide.

## Display

Expand Down
1 change: 1 addition & 0 deletions docs/reference/renderers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ CSS), not missing functionality.
| ------------------------------ | :----: | :-: | ---------------------------------------------------------------------------------------------- |
| Drag to move / resize / create | ✅ | ✅ | native: long-press + gesture; dom: pointer. |
| Month drag (create / move day) | ✅ | ✅ | Sweep an all-day span, or drop an event on another day. Resize is time-grid only. |
| Year drag (select / create) | ✅ | ✅ | Sweep a date range across mini months; native keeps a sweep inside the month it started in. |
| Pinch / scroll to zoom | ✅ | ✅ | native: pinch; dom: pinch + Ctrl/⌘-scroll (`zoomable`). |
| Controlled navigation | ✅ | ✅ | both fire `onChangeDate` — native on swipe/paging, dom on PageDown/PageUp of the focused grid. |
| RTL layout | ✅ | ❌ | native `isRTL` (cosmetic). |
Expand Down
16 changes: 16 additions & 0 deletions packages/dom/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ export interface CalendarProps<T = unknown>
keyboardDayNavigation?: boolean;
/** Tap a day cell. */
onPressDay?: (date: Date) => void;
/**
* Month and year modes: reports a create sweep's day span as it happens, as the
* ordered inclusive `[start, end]` days, so a selection highlight can follow the
* drag. Pair it with `useDateRange`'s `selectRange`. Enables the sweep on its
* own, so it works without `onCreateEvent`.
*/
onSelectDrag?: (start: Date, end: Date) => void;
/** Tap a month's title in the year view — e.g. jump to that month. */
onPressMonth?: (month: Date) => void;
/** Tap the "+N more" overflow row. */
Expand Down Expand Up @@ -335,6 +342,7 @@ export function Calendar<T = unknown>({
isDateDisabled,
keyboardDayNavigation,
onPressDay,
onSelectDrag,
onPressMonth,
onPressMore,
renderMonthEvent,
Expand Down Expand Up @@ -388,8 +396,15 @@ export function Calendar<T = unknown>({
style={height != null ? { height, ...style } : style}
classNames={classNames}
styles={styles}
selectedRange={selectedRange}
selectedDates={selectedDates}
minDate={minDate}
maxDate={maxDate}
isDateDisabled={isDateDisabled}
onPressDay={onPressDay}
onPressMonth={onPressMonth}
onSelectDrag={onSelectDrag}
onCreateEvent={onCreateEvent}
/>
);
} else if (mode === "schedule") {
Expand Down Expand Up @@ -440,6 +455,7 @@ export function Calendar<T = unknown>({
onPressDay={onPressDay}
onPressCell={onPressCell}
onCreateEvent={onCreateEvent}
onSelectDrag={onSelectDrag}
onDragEvent={onDragEvent}
onDragStart={onDragStart}
eventStartEditable={eventStartEditable}
Expand Down
Loading