@savvycal/calendar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +472 -0
- package/dist/components/resource-grid-view/AllDayRow.d.ts +12 -0
- package/dist/components/resource-grid-view/AllDayRow.d.ts.map +1 -0
- package/dist/components/resource-grid-view/AllDayRow.js +54 -0
- package/dist/components/resource-grid-view/EventChip.d.ts +22 -0
- package/dist/components/resource-grid-view/EventChip.d.ts.map +1 -0
- package/dist/components/resource-grid-view/EventChip.js +112 -0
- package/dist/components/resource-grid-view/GridHeader.d.ts +13 -0
- package/dist/components/resource-grid-view/GridHeader.d.ts.map +1 -0
- package/dist/components/resource-grid-view/GridHeader.js +31 -0
- package/dist/components/resource-grid-view/NowIndicator.d.ts +13 -0
- package/dist/components/resource-grid-view/NowIndicator.d.ts.map +1 -0
- package/dist/components/resource-grid-view/NowIndicator.js +43 -0
- package/dist/components/resource-grid-view/ResourceColumn.d.ts +22 -0
- package/dist/components/resource-grid-view/ResourceColumn.d.ts.map +1 -0
- package/dist/components/resource-grid-view/ResourceColumn.js +52 -0
- package/dist/components/resource-grid-view/ResourceGridView.d.ts +3 -0
- package/dist/components/resource-grid-view/ResourceGridView.d.ts.map +1 -0
- package/dist/components/resource-grid-view/ResourceGridView.js +342 -0
- package/dist/components/resource-grid-view/SelectionOverlay.d.ts +22 -0
- package/dist/components/resource-grid-view/SelectionOverlay.d.ts.map +1 -0
- package/dist/components/resource-grid-view/SelectionOverlay.js +87 -0
- package/dist/components/resource-grid-view/SlotInteractionLayer.d.ts +28 -0
- package/dist/components/resource-grid-view/SlotInteractionLayer.d.ts.map +1 -0
- package/dist/components/resource-grid-view/SlotInteractionLayer.js +213 -0
- package/dist/components/resource-grid-view/TimeGutter.d.ts +11 -0
- package/dist/components/resource-grid-view/TimeGutter.d.ts.map +1 -0
- package/dist/components/resource-grid-view/TimeGutter.js +25 -0
- package/dist/components/resource-grid-view/UnavailabilityOverlay.d.ts +10 -0
- package/dist/components/resource-grid-view/UnavailabilityOverlay.d.ts.map +1 -0
- package/dist/components/resource-grid-view/UnavailabilityOverlay.js +37 -0
- package/dist/components/resource-grid-view/defaults.d.ts +3 -0
- package/dist/components/resource-grid-view/defaults.d.ts.map +1 -0
- package/dist/components/resource-grid-view/defaults.js +28 -0
- package/dist/components/resource-grid-view/index.d.ts +3 -0
- package/dist/components/resource-grid-view/index.d.ts.map +1 -0
- package/dist/components/resource-grid-view/useAnnouncer.d.ts +5 -0
- package/dist/components/resource-grid-view/useAnnouncer.d.ts.map +1 -0
- package/dist/components/resource-grid-view/useAnnouncer.js +12 -0
- package/dist/components/resource-grid-view/useEffectiveHourHeight.d.ts +15 -0
- package/dist/components/resource-grid-view/useEffectiveHourHeight.d.ts.map +1 -0
- package/dist/components/resource-grid-view/useEffectiveHourHeight.js +20 -0
- package/dist/components.css +31 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/lib/availability.d.ts +12 -0
- package/dist/lib/availability.d.ts.map +1 -0
- package/dist/lib/availability.js +56 -0
- package/dist/lib/overlap.d.ts +16 -0
- package/dist/lib/overlap.d.ts.map +1 -0
- package/dist/lib/overlap.js +80 -0
- package/dist/lib/selection.d.ts +4 -0
- package/dist/lib/selection.d.ts.map +1 -0
- package/dist/lib/selection.js +14 -0
- package/dist/lib/time.d.ts +21 -0
- package/dist/lib/time.d.ts.map +1 -0
- package/dist/lib/time.js +52 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +8 -0
- package/dist/preset.css +93 -0
- package/dist/types/calendar.d.ts +128 -0
- package/dist/types/calendar.d.ts.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SavvyCal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# @savvycal/calendar
|
|
2
|
+
|
|
3
|
+
A fully-featured resource grid calendar component built with React, Tailwind CSS v4, and the Temporal API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @savvycal/calendar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer dependencies
|
|
12
|
+
|
|
13
|
+
- `react` ^18 || ^19
|
|
14
|
+
- `react-dom` ^18 || ^19
|
|
15
|
+
|
|
16
|
+
The package ships [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill) as a direct dependency, so you don't need to install it separately.
|
|
17
|
+
|
|
18
|
+
## CSS setup (Tailwind CSS v4)
|
|
19
|
+
|
|
20
|
+
Add three imports to your app's CSS **in this exact order**:
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@import '@savvycal/calendar/preset.css';
|
|
24
|
+
@import 'tailwindcss';
|
|
25
|
+
@import '@savvycal/calendar/components.css';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Why order matters:** The preset registers `@theme` tokens and a `@source` directive _before_ Tailwind processes them, so Tailwind can generate the utility classes the library uses. The components stylesheet must come _after_ Tailwind so its custom CSS (e.g. the unavailable-time cross-hatch pattern) can reference Tailwind's generated values.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { ResourceGridView, Temporal } from '@savvycal/calendar';
|
|
34
|
+
|
|
35
|
+
const today = Temporal.Now.plainDateISO();
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
return (
|
|
39
|
+
<ResourceGridView
|
|
40
|
+
date={today}
|
|
41
|
+
timeZone="America/Chicago"
|
|
42
|
+
resources={[
|
|
43
|
+
{ id: '1', name: 'Alice', color: '#3b82f6' },
|
|
44
|
+
{ id: '2', name: 'Bob', color: '#8b5cf6' },
|
|
45
|
+
]}
|
|
46
|
+
events={[
|
|
47
|
+
{
|
|
48
|
+
id: 'evt-1',
|
|
49
|
+
title: 'Meeting',
|
|
50
|
+
resourceId: '1',
|
|
51
|
+
startTime: today
|
|
52
|
+
.toPlainDateTime({ hour: 10 })
|
|
53
|
+
.toZonedDateTime('America/Chicago'),
|
|
54
|
+
endTime: today
|
|
55
|
+
.toPlainDateTime({ hour: 11 })
|
|
56
|
+
.toZonedDateTime('America/Chicago'),
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## ResourceGridView props
|
|
65
|
+
|
|
66
|
+
### Required
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Description |
|
|
69
|
+
| ----------- | -------------------- | ----------------------------------------------------- |
|
|
70
|
+
| `date` | `Temporal.PlainDate` | The date to display. |
|
|
71
|
+
| `timeZone` | `string` | IANA time zone identifier (e.g. `"America/Chicago"`). |
|
|
72
|
+
| `resources` | `CalendarResource[]` | Array of resources (columns). |
|
|
73
|
+
| `events` | `CalendarEvent[]` | Array of timed and/or all-day events. |
|
|
74
|
+
|
|
75
|
+
### Time axis
|
|
76
|
+
|
|
77
|
+
| Prop | Type | Default | Description |
|
|
78
|
+
| ---------- | ---------------- | ---------------------------------------------------- | ---------------------------------------------------- |
|
|
79
|
+
| `timeAxis` | `TimeAxisConfig` | `{ startHour: 0, endHour: 24, intervalMinutes: 60 }` | Controls the visible hour range and gutter interval. |
|
|
80
|
+
|
|
81
|
+
### Layout
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default | Description |
|
|
84
|
+
| ---------------- | ---------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `hourHeight` | `number` | `60` | Height in pixels for one hour. |
|
|
86
|
+
| `columnMinWidth` | `number` | `120` | Minimum width in pixels for each resource column. |
|
|
87
|
+
| `eventLayout` | `'columns' \| 'stacked'` | `'columns'` | How overlapping events are laid out. `'columns'` places them side-by-side; `'stacked'` offsets them. |
|
|
88
|
+
| `eventGap` | `number` | — | Vertical gap in pixels between the edge of events and column borders. |
|
|
89
|
+
| `stackOffset` | `number` | `8` | Horizontal pixel offset for each stacked event (only applies when `eventLayout` is `'stacked'`). |
|
|
90
|
+
| `className` | `string` | — | Class name applied to the root element. |
|
|
91
|
+
| `classNames` | `ResourceGridViewClassNames` | See [Customizing styles](#customizing-styles). | Override default class names for internal elements. |
|
|
92
|
+
|
|
93
|
+
### Events & interaction
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Default | Description |
|
|
96
|
+
| --------------------- | -------------------------------------------------- | ------- | -------------------------------------------------------------------- |
|
|
97
|
+
| `onEventClick` | `(event: CalendarEvent) => void` | — | Called when an event is clicked. |
|
|
98
|
+
| `onSlotClick` | `(info: { resource, startTime, endTime }) => void` | — | Called when an empty time slot is clicked. |
|
|
99
|
+
| `snapDuration` | `number` | — | Snap interval in minutes for drag selection. |
|
|
100
|
+
| `placeholderDuration` | `number` | `15` | Duration in minutes for the hover placeholder shown before dragging. |
|
|
101
|
+
|
|
102
|
+
### Selection
|
|
103
|
+
|
|
104
|
+
| Prop | Type | Default | Description |
|
|
105
|
+
| ----------------------- | ---------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `selectedRange` | `SelectedRange \| null` | — | The currently selected time range (controlled). |
|
|
107
|
+
| `onSelect` | `(range: SelectedRange \| null) => void` | — | Called when the user selects or clears a range by dragging. |
|
|
108
|
+
| `selectionAppearance` | `SelectionAppearance` | — | How the selection is rendered. `'highlight'` shows a translucent overlay; `{ style: 'event', eventData? }` renders it as a phantom event. |
|
|
109
|
+
| `dragPreviewAppearance` | `SelectionAppearance` | — | Appearance of the drag preview while the user is actively dragging (before releasing). Falls back to `selectionAppearance` if not set. |
|
|
110
|
+
| `selectionRef` | `Ref<HTMLDivElement>` | — | Ref attached to the selection element, useful for positioning popovers (e.g. with [Floating UI](https://floating-ui.com)). |
|
|
111
|
+
| `selectedEventId` | `string \| null` | — | ID of the currently selected event (controlled). Applies selected styling and enables `selectedEventRef`. |
|
|
112
|
+
| `selectedEventRef` | `Ref<HTMLDivElement>` | — | Ref attached to the selected event element, useful for positioning popovers. |
|
|
113
|
+
|
|
114
|
+
### Availability
|
|
115
|
+
|
|
116
|
+
| Prop | Type | Default | Description |
|
|
117
|
+
| ---------------- | ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `availability` | `Record<string, AvailabilityRange[]>` | — | Map of resource ID to available time ranges. Times outside these ranges are shown as unavailable. |
|
|
119
|
+
| `unavailability` | `Record<string, AvailabilityRange[]>` | — | Map of resource ID to explicitly unavailable time ranges. Applied on top of availability. |
|
|
120
|
+
|
|
121
|
+
### Render props
|
|
122
|
+
|
|
123
|
+
| Prop | Type | Description |
|
|
124
|
+
| -------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
125
|
+
| `renderHeader` | `(props: { resource: CalendarResource }) => ReactNode` | Custom renderer for resource column headers. |
|
|
126
|
+
| `renderEvent` | `(props: { event: TimedCalendarEvent, position: PositionedEvent }) => ReactNode` | Custom renderer for timed events. |
|
|
127
|
+
| `renderCorner` | `() => ReactNode` | Custom renderer for the top-left corner cell (e.g. time zone label). |
|
|
128
|
+
|
|
129
|
+
## Data types
|
|
130
|
+
|
|
131
|
+
### CalendarResource
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
interface CalendarResource {
|
|
135
|
+
id: string;
|
|
136
|
+
name: string;
|
|
137
|
+
avatarUrl?: string;
|
|
138
|
+
color?: string;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### CalendarEvent
|
|
143
|
+
|
|
144
|
+
A discriminated union of `TimedCalendarEvent` and `AllDayCalendarEvent`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
interface TimedCalendarEvent {
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
resourceId: string;
|
|
151
|
+
startTime: Temporal.ZonedDateTime;
|
|
152
|
+
endTime: Temporal.ZonedDateTime;
|
|
153
|
+
allDay?: false;
|
|
154
|
+
color?: string;
|
|
155
|
+
clientName?: string;
|
|
156
|
+
status?: 'confirmed' | 'canceled' | 'tentative';
|
|
157
|
+
metadata?: Record<string, unknown>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface AllDayCalendarEvent {
|
|
161
|
+
id: string;
|
|
162
|
+
title: string;
|
|
163
|
+
resourceId: string;
|
|
164
|
+
startDate: Temporal.PlainDate;
|
|
165
|
+
endDate: Temporal.PlainDate;
|
|
166
|
+
allDay: true;
|
|
167
|
+
color?: string;
|
|
168
|
+
clientName?: string;
|
|
169
|
+
status?: 'confirmed' | 'canceled' | 'tentative';
|
|
170
|
+
metadata?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### AvailabilityRange
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
interface AvailabilityRange {
|
|
178
|
+
startTime: Temporal.ZonedDateTime;
|
|
179
|
+
endTime: Temporal.ZonedDateTime;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### SelectedRange
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
interface SelectedRange {
|
|
187
|
+
resourceId: string;
|
|
188
|
+
startTime: Temporal.ZonedDateTime;
|
|
189
|
+
endTime: Temporal.ZonedDateTime;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### TimeAxisConfig
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
interface TimeAxisConfig {
|
|
197
|
+
startHour?: number; // default: 0
|
|
198
|
+
endHour?: number; // default: 24
|
|
199
|
+
intervalMinutes?: number; // default: 60
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### PositionedEvent
|
|
204
|
+
|
|
205
|
+
Provided to the `renderEvent` render prop with layout information:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
interface PositionedEvent {
|
|
209
|
+
event: TimedCalendarEvent;
|
|
210
|
+
top: number;
|
|
211
|
+
height: number;
|
|
212
|
+
subColumn: number;
|
|
213
|
+
totalSubColumns: number;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Customizing styles
|
|
218
|
+
|
|
219
|
+
### CSS custom properties
|
|
220
|
+
|
|
221
|
+
The preset defines a set of `--color-cal-*` CSS custom properties under `@theme`. Override them in your own CSS to change the calendar's color palette:
|
|
222
|
+
|
|
223
|
+
| Variable | Light default | Description |
|
|
224
|
+
| --------------------------------- | ---------------- | --------------------------------- |
|
|
225
|
+
| `--color-cal-surface` | `white` | Background color of the calendar. |
|
|
226
|
+
| `--color-cal-border` | `zinc-300` | Border color for grid lines. |
|
|
227
|
+
| `--color-cal-text` | `zinc-950` | Primary text color (headers). |
|
|
228
|
+
| `--color-cal-text-body` | `zinc-900` | Body text color (event titles). |
|
|
229
|
+
| `--color-cal-text-muted` | `zinc-600` | Muted text color (times, labels). |
|
|
230
|
+
| `--color-cal-event-bg` | `zinc-100 @ 90%` | Event background. |
|
|
231
|
+
| `--color-cal-event-ring` | `zinc-900 @ 15%` | Event border ring. |
|
|
232
|
+
| `--color-cal-event-ring-selected` | `zinc-900 @ 30%` | Ring color for selected events. |
|
|
233
|
+
| `--color-cal-event-shadow` | `black @ 10%` | Shadow color for selected events. |
|
|
234
|
+
| `--color-cal-now` | `orange-500` | Now indicator line color. |
|
|
235
|
+
| `--color-cal-slot-highlight` | `blue-400 @ 15%` | Hover slot highlight. |
|
|
236
|
+
| `--color-cal-selection` | `blue-400 @ 25%` | Drag selection highlight. |
|
|
237
|
+
|
|
238
|
+
Example override:
|
|
239
|
+
|
|
240
|
+
```css
|
|
241
|
+
@import '@savvycal/calendar/preset.css';
|
|
242
|
+
@import 'tailwindcss';
|
|
243
|
+
@import '@savvycal/calendar/components.css';
|
|
244
|
+
|
|
245
|
+
@theme {
|
|
246
|
+
--color-cal-now: var(--color-red-500);
|
|
247
|
+
--color-cal-selection: color-mix(
|
|
248
|
+
in oklab,
|
|
249
|
+
var(--color-indigo-400) 25%,
|
|
250
|
+
transparent
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Dark mode
|
|
256
|
+
|
|
257
|
+
The preset uses a `.dark` class convention (matching Tailwind's `@custom-variant dark`). Add the `dark` class to a parent element to switch to dark mode. All `--color-cal-*` variables are automatically overridden in the `.dark` scope.
|
|
258
|
+
|
|
259
|
+
### The `classNames` prop
|
|
260
|
+
|
|
261
|
+
Every internal element can be restyled via the `classNames` prop. Each key maps to a specific part of the calendar:
|
|
262
|
+
|
|
263
|
+
| Key | Description |
|
|
264
|
+
| -------------------- | ------------------------------------------------ |
|
|
265
|
+
| `root` | Outermost wrapper (scroll container). |
|
|
266
|
+
| `grid` | The CSS grid element. |
|
|
267
|
+
| `cornerCell` | Top-left corner cell (sticky). |
|
|
268
|
+
| `headerCell` | Resource column header cell (sticky). |
|
|
269
|
+
| `headerName` | Resource name text inside the header. |
|
|
270
|
+
| `headerAvatar` | Avatar image inside the header. |
|
|
271
|
+
| `gutterCell` | Time gutter cell for hour labels (sticky). |
|
|
272
|
+
| `gutterCellMinor` | Time gutter cell for sub-hour intervals. |
|
|
273
|
+
| `gutterLabel` | Text label inside gutter cells. |
|
|
274
|
+
| `bodyCell` | Hour-start body cell in the grid. |
|
|
275
|
+
| `bodyCellMinor` | Sub-hour body cell in the grid. |
|
|
276
|
+
| `eventColumn` | Container for events within a resource column. |
|
|
277
|
+
| `event` | Individual event element. |
|
|
278
|
+
| `eventSelected` | Additional classes applied to a selected event. |
|
|
279
|
+
| `eventColorBar` | Vertical color bar on the left edge of an event. |
|
|
280
|
+
| `eventTitle` | Event title text. |
|
|
281
|
+
| `eventTime` | Event time text. |
|
|
282
|
+
| `eventClientName` | Client name text on the event. |
|
|
283
|
+
| `nowIndicator` | Horizontal "now" indicator line. |
|
|
284
|
+
| `slotHighlight` | Hover highlight on time slots. |
|
|
285
|
+
| `selectionHighlight` | Drag selection highlight overlay. |
|
|
286
|
+
| `allDayCell` | All-day event row cell. |
|
|
287
|
+
| `unavailableOverlay` | Unavailable time cross-hatch overlay. |
|
|
288
|
+
|
|
289
|
+
### `resourceGridViewDefaults`
|
|
290
|
+
|
|
291
|
+
The library exports `resourceGridViewDefaults`, an object containing the default Tailwind classes for every `classNames` key. Use it to extend rather than replace defaults:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import {
|
|
295
|
+
ResourceGridView,
|
|
296
|
+
resourceGridViewDefaults,
|
|
297
|
+
cn,
|
|
298
|
+
} from '@savvycal/calendar';
|
|
299
|
+
|
|
300
|
+
<ResourceGridView
|
|
301
|
+
classNames={{
|
|
302
|
+
event: cn(resourceGridViewDefaults.event, 'rounded-lg'),
|
|
303
|
+
headerCell: cn(resourceGridViewDefaults.headerCell, 'border-b'),
|
|
304
|
+
}}
|
|
305
|
+
// ...other props
|
|
306
|
+
/>;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The `cn()` utility (re-exported from the library) merges Tailwind classes with conflict resolution via `tailwind-merge`.
|
|
310
|
+
|
|
311
|
+
## Selection & interaction
|
|
312
|
+
|
|
313
|
+
### Controlled selection
|
|
314
|
+
|
|
315
|
+
Selection is controlled via `selectedRange` and `onSelect`:
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const [selectedRange, setSelectedRange] = useState<SelectedRange | null>(null);
|
|
319
|
+
|
|
320
|
+
<ResourceGridView
|
|
321
|
+
selectedRange={selectedRange}
|
|
322
|
+
onSelect={setSelectedRange}
|
|
323
|
+
// ...
|
|
324
|
+
/>;
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Selection appearance
|
|
328
|
+
|
|
329
|
+
- `'highlight'` — renders a translucent overlay on the selected time range.
|
|
330
|
+
- `{ style: 'event', eventData? }` — renders the selection as a phantom event. Pass `eventData` to customize its title, color, etc.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
<ResourceGridView
|
|
334
|
+
selectionAppearance={{
|
|
335
|
+
style: 'event',
|
|
336
|
+
eventData: {
|
|
337
|
+
title: 'New appointment',
|
|
338
|
+
color: '#3b82f6',
|
|
339
|
+
},
|
|
340
|
+
}}
|
|
341
|
+
// ...
|
|
342
|
+
/>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Popover positioning with `selectionRef`
|
|
346
|
+
|
|
347
|
+
Attach `selectionRef` to use [Floating UI](https://floating-ui.com) (or a similar library) to position a popover next to the selection:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
import {
|
|
351
|
+
useFloating,
|
|
352
|
+
autoUpdate,
|
|
353
|
+
flip,
|
|
354
|
+
shift,
|
|
355
|
+
offset,
|
|
356
|
+
} from '@floating-ui/react';
|
|
357
|
+
|
|
358
|
+
const { refs, floatingStyles } = useFloating({
|
|
359
|
+
open: selectedRange !== null,
|
|
360
|
+
middleware: [flip(), shift(), offset(5)],
|
|
361
|
+
placement: 'right',
|
|
362
|
+
whileElementsMounted: autoUpdate,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
<ResourceGridView
|
|
366
|
+
selectionRef={refs.setReference}
|
|
367
|
+
selectedRange={selectedRange}
|
|
368
|
+
onSelect={setSelectedRange}
|
|
369
|
+
// ...
|
|
370
|
+
/>;
|
|
371
|
+
|
|
372
|
+
{
|
|
373
|
+
selectedRange && (
|
|
374
|
+
<div ref={refs.setFloating} style={floatingStyles}>
|
|
375
|
+
{/* Popover content */}
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Event selection & popover positioning
|
|
382
|
+
|
|
383
|
+
Use `selectedEventId` and `selectedEventRef` to track which event is selected and position a popover next to it:
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
|
387
|
+
|
|
388
|
+
const { refs, floatingStyles } = useFloating({
|
|
389
|
+
open: selectedEventId !== null,
|
|
390
|
+
onOpenChange: (open) => {
|
|
391
|
+
if (!open) setSelectedEventId(null);
|
|
392
|
+
},
|
|
393
|
+
middleware: [flip(), shift(), offset(5)],
|
|
394
|
+
placement: 'right',
|
|
395
|
+
whileElementsMounted: autoUpdate,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
<ResourceGridView
|
|
399
|
+
selectedEventId={selectedEventId}
|
|
400
|
+
selectedEventRef={refs.setReference}
|
|
401
|
+
onEventClick={(event) => setSelectedEventId(event.id)}
|
|
402
|
+
// ...
|
|
403
|
+
/>;
|
|
404
|
+
|
|
405
|
+
{
|
|
406
|
+
selectedEventId && (
|
|
407
|
+
<div ref={refs.setFloating} style={floatingStyles}>
|
|
408
|
+
{/* Event popover content */}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Slot and event clicks
|
|
415
|
+
|
|
416
|
+
- `onSlotClick` fires when clicking an empty time slot, receiving the `resource`, `startTime`, and `endTime`.
|
|
417
|
+
- `onEventClick` fires when clicking an event.
|
|
418
|
+
- `snapDuration` (in minutes) controls the snap interval for drag-to-select.
|
|
419
|
+
- `placeholderDuration` (in minutes, default `15`) controls the height of the hover placeholder shown before the user starts dragging.
|
|
420
|
+
|
|
421
|
+
## Exports
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// Components
|
|
425
|
+
export { ResourceGridView } from '@savvycal/calendar';
|
|
426
|
+
// Defaults
|
|
427
|
+
export { resourceGridViewDefaults } from '@savvycal/calendar';
|
|
428
|
+
|
|
429
|
+
// Temporal polyfill
|
|
430
|
+
export { Temporal } from '@savvycal/calendar';
|
|
431
|
+
|
|
432
|
+
// Utility
|
|
433
|
+
export { cn } from '@savvycal/calendar';
|
|
434
|
+
|
|
435
|
+
// Types
|
|
436
|
+
export type {
|
|
437
|
+
CalendarResource,
|
|
438
|
+
CalendarEvent,
|
|
439
|
+
TimedCalendarEvent,
|
|
440
|
+
AllDayCalendarEvent,
|
|
441
|
+
TimeSlot,
|
|
442
|
+
AvailabilityRange,
|
|
443
|
+
TimeAxisConfig,
|
|
444
|
+
ResourceGridViewProps,
|
|
445
|
+
ResourceGridViewClassNames,
|
|
446
|
+
PositionedEvent,
|
|
447
|
+
SelectedRange,
|
|
448
|
+
SelectionAppearance,
|
|
449
|
+
SelectionEventData,
|
|
450
|
+
EventLayout,
|
|
451
|
+
} from '@savvycal/calendar';
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Accessibility
|
|
455
|
+
|
|
456
|
+
The calendar includes built-in ARIA support for screen readers:
|
|
457
|
+
|
|
458
|
+
- **Grid region** — The grid container has `role="region"` with `aria-roledescription="calendar"` and a descriptive `aria-label` (e.g. "Schedule for Monday, February 9, 2026").
|
|
459
|
+
- **Column headers** — Each resource header has `role="columnheader"`.
|
|
460
|
+
- **Event labels** — Interactive events include an `aria-label` with the event title, time range, client name, and status (if canceled or tentative). All-day events are labeled with the title and "all day".
|
|
461
|
+
- **Live region announcements** — A visually-hidden `aria-live="polite"` region announces state changes:
|
|
462
|
+
- Selecting an event: `"Selected: Meeting, 2 pm to 3 pm, Jane Doe"`
|
|
463
|
+
- Selecting a time range: `"Selected time: 2 pm to 3 pm, Dr. Smith"`
|
|
464
|
+
- **Decorative overlays** — The now indicator, unavailability overlays, selection overlay, and slot highlights are marked `aria-hidden="true"` to reduce noise.
|
|
465
|
+
|
|
466
|
+
### Custom renderers
|
|
467
|
+
|
|
468
|
+
When using `renderEvent`, the library does **not** add `aria-label` or `role` attributes to the wrapper `<div>`. Your custom renderer is responsible for providing its own accessible markup.
|
|
469
|
+
|
|
470
|
+
## License
|
|
471
|
+
|
|
472
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
import { AllDayCalendarEvent, CalendarEvent, ResourceGridViewClassNames } from '../../types/calendar';
|
|
3
|
+
interface AllDayRowProps {
|
|
4
|
+
events: AllDayCalendarEvent[];
|
|
5
|
+
cls: (key: keyof ResourceGridViewClassNames) => string;
|
|
6
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
7
|
+
selectedEventId?: string | null;
|
|
8
|
+
selectedEventRef?: Ref<HTMLDivElement>;
|
|
9
|
+
}
|
|
10
|
+
export declare const AllDayRow: import('react').NamedExoticComponent<AllDayRowProps>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=AllDayRow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AllDayRow.d.ts","sourceRoot":"","sources":["../../../src/components/resource-grid-view/AllDayRow.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,0BAA0B,EAC3B,MAAM,kBAAkB,CAAC;AAE1B,UAAU,cAAc;IACtB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,0BAA0B,KAAK,MAAM,CAAC;IACvD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,SAAS,sDAwDpB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as l, Fragment as m, jsxs as d } from "react/jsx-runtime";
|
|
2
|
+
import { memo as u } from "react";
|
|
3
|
+
import { cn as f } from "../../lib/utils.js";
|
|
4
|
+
const x = u(function({
|
|
5
|
+
events: o,
|
|
6
|
+
cls: i,
|
|
7
|
+
onEventClick: a,
|
|
8
|
+
selectedEventId: c,
|
|
9
|
+
selectedEventRef: s
|
|
10
|
+
}) {
|
|
11
|
+
return o.length === 0 ? null : /* @__PURE__ */ l(m, { children: o.map((t) => {
|
|
12
|
+
const r = t.id === c, e = [t.title, "all day"];
|
|
13
|
+
t.clientName && e.push(t.clientName);
|
|
14
|
+
const n = /* @__PURE__ */ d(
|
|
15
|
+
"button",
|
|
16
|
+
{
|
|
17
|
+
type: "button",
|
|
18
|
+
"aria-label": e.join(", "),
|
|
19
|
+
className: f(i("event"), r && i("eventSelected")),
|
|
20
|
+
style: {
|
|
21
|
+
position: "relative",
|
|
22
|
+
inset: "unset",
|
|
23
|
+
cursor: "pointer",
|
|
24
|
+
flex: "1 1 0",
|
|
25
|
+
minWidth: 0
|
|
26
|
+
},
|
|
27
|
+
onClick: () => a?.(t),
|
|
28
|
+
children: [
|
|
29
|
+
t.color && /* @__PURE__ */ l(
|
|
30
|
+
"div",
|
|
31
|
+
{
|
|
32
|
+
className: i("eventColorBar"),
|
|
33
|
+
style: { backgroundColor: t.color }
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
/* @__PURE__ */ l("div", { className: i("eventTitle"), children: t.title })
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
t.id
|
|
40
|
+
);
|
|
41
|
+
return r ? /* @__PURE__ */ l(
|
|
42
|
+
"div",
|
|
43
|
+
{
|
|
44
|
+
ref: s,
|
|
45
|
+
style: { flex: "1 1 0", minWidth: 0 },
|
|
46
|
+
children: n
|
|
47
|
+
},
|
|
48
|
+
t.id
|
|
49
|
+
) : n;
|
|
50
|
+
}) });
|
|
51
|
+
});
|
|
52
|
+
export {
|
|
53
|
+
x as AllDayRow
|
|
54
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
import { CalendarEvent, CalendarResource, EventLayout, PositionedEvent, ResourceGridViewClassNames, TimedCalendarEvent } from '../../types/calendar';
|
|
3
|
+
interface EventChipProps {
|
|
4
|
+
positioned: PositionedEvent;
|
|
5
|
+
resource: CalendarResource;
|
|
6
|
+
timeZone: string;
|
|
7
|
+
cls: (key: keyof ResourceGridViewClassNames) => string;
|
|
8
|
+
onClick?: (event: CalendarEvent) => void;
|
|
9
|
+
renderEvent?: (props: {
|
|
10
|
+
event: TimedCalendarEvent;
|
|
11
|
+
position: PositionedEvent;
|
|
12
|
+
}) => React.ReactNode;
|
|
13
|
+
interactive?: boolean;
|
|
14
|
+
eventGap?: number;
|
|
15
|
+
eventLayout?: EventLayout;
|
|
16
|
+
stackOffset?: number;
|
|
17
|
+
isSelected?: boolean;
|
|
18
|
+
selectedEventRef?: Ref<HTMLDivElement>;
|
|
19
|
+
}
|
|
20
|
+
export declare const EventChip: import('react').NamedExoticComponent<EventChipProps>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=EventChip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventChip.d.ts","sourceRoot":"","sources":["../../../src/components/resource-grid-view/EventChip.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,0BAA0B,EAC1B,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAG1B,UAAU,cAAc;IACtB,UAAU,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,0BAA0B,KAAK,MAAM,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QACpB,KAAK,EAAE,kBAAkB,CAAC;QAC1B,QAAQ,EAAE,eAAe,CAAC;KAC3B,KAAK,KAAK,CAAC,SAAS,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,SAAS,sDAsJpB,CAAC"}
|