@gobrand/react-calendar 0.0.9 → 0.0.11
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/README.md +717 -152
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
# @gobrand/react-calendar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@gobrand/react-calendar)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
React hook for building powerful calendar views with the [Temporal API](https://tc39.es/proposal-temporal/docs/). Built on [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) with optimized state management using TanStack Store.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
7
10
|
```bash
|
|
11
|
+
npm install @gobrand/react-calendar
|
|
12
|
+
# or
|
|
8
13
|
pnpm add @gobrand/react-calendar
|
|
9
14
|
```
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
16
|
+
**Peer dependencies:**
|
|
17
|
+
- `react ^18.0.0 || ^19.0.0`
|
|
18
|
+
|
|
19
|
+
## Why @gobrand/react-calendar?
|
|
20
|
+
|
|
21
|
+
Building React calendar UIs is complex: state management, timezone handling, DST transitions, multi-view navigation, and data mapping. **@gobrand/react-calendar** provides a complete React solution:
|
|
22
|
+
|
|
23
|
+
- **🪝 Simple React Hook** - Single `useCalendar()` hook with reactive state management
|
|
24
|
+
- **📅 Multi-view support** - Month, week, and day views with type-safe view switching
|
|
25
|
+
- **🌍 Timezone-aware** - Native timezone support with Temporal API primitives
|
|
26
|
+
- **🎯 Data-agnostic** - Works with any data type through accessor pattern
|
|
27
|
+
- **⚡️ Type-safe** - Full TypeScript support with conditional types based on configured views
|
|
28
|
+
- **⚙️ TanStack Store** - Optimized state management with TanStack Store
|
|
29
|
+
- **🔧 Zero config** - Sensible defaults, customize only what you need
|
|
30
|
+
- **📦 Minimal** - Built on [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) for framework-agnostic logic
|
|
31
|
+
|
|
32
|
+
**Key features:**
|
|
33
|
+
- ✅ Built exclusively on Temporal API (no Date objects, no moment.js, no date-fns)
|
|
34
|
+
- ✅ Automatic DST handling and timezone conversions
|
|
35
|
+
- ✅ Type-safe view methods (only available methods for configured views)
|
|
36
|
+
- ✅ Calendar-aware arithmetic (leap years, month-end dates)
|
|
37
|
+
- ✅ Flexible accessor pattern for any data structure
|
|
38
|
+
- ✅ Polyfill included for browser compatibility
|
|
39
|
+
- ✅ All core utilities re-exported for convenience
|
|
40
|
+
|
|
41
|
+
**Perfect for:**
|
|
42
|
+
- React event calendars and schedulers
|
|
43
|
+
- Booking systems and appointment managers
|
|
44
|
+
- Task management with due dates
|
|
45
|
+
- Analytics dashboards with date ranges
|
|
46
|
+
- Any React app that needs calendar navigation
|
|
17
47
|
|
|
18
48
|
## Quick Start
|
|
19
49
|
|
|
20
50
|
```tsx
|
|
21
|
-
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
51
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor, getWeekdays } from '@gobrand/react-calendar';
|
|
22
52
|
import { Temporal } from '@js-temporal/polyfill';
|
|
23
53
|
|
|
24
54
|
type Event = {
|
|
@@ -27,18 +57,24 @@ type Event = {
|
|
|
27
57
|
start: Temporal.ZonedDateTime;
|
|
28
58
|
};
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
const events: Event[] = [
|
|
61
|
+
{
|
|
62
|
+
id: '1',
|
|
63
|
+
title: 'Team Meeting',
|
|
64
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T14:00:00-05:00[America/New_York]')
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
31
68
|
const accessor = createCalendarAccessor<Event>({
|
|
32
69
|
getDate: (event) => event.start.toPlainDate(),
|
|
33
70
|
getStart: (event) => event.start,
|
|
34
|
-
getEnd: (event) => event.start,
|
|
35
71
|
});
|
|
36
72
|
|
|
37
73
|
function MyCalendar() {
|
|
38
74
|
const calendar = useCalendar({
|
|
39
75
|
data: events,
|
|
40
|
-
views: createCalendarViews({
|
|
41
|
-
month: {
|
|
76
|
+
views: createCalendarViews<Event>()({
|
|
77
|
+
month: { accessor },
|
|
42
78
|
}),
|
|
43
79
|
});
|
|
44
80
|
|
|
@@ -78,126 +114,336 @@ function MyCalendar() {
|
|
|
78
114
|
}
|
|
79
115
|
```
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
> **Note:** For vanilla JavaScript/TypeScript usage, check out [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) which provides the underlying framework-agnostic functions.
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### React Hooks
|
|
122
|
+
|
|
123
|
+
#### `useCalendar(options)`
|
|
82
124
|
|
|
125
|
+
Create a calendar instance with reactive state management. Returns a Calendar object with type-safe methods based on configured views.
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
- `options` (CalendarOptions): Configuration object
|
|
129
|
+
- `data` (TItem[]): Array of items to display in the calendar
|
|
130
|
+
- `views` (CalendarViewOptions): View configurations (month, week, day)
|
|
131
|
+
- `timeZone` (string, optional): IANA timezone identifier (defaults to system timezone)
|
|
132
|
+
- `state` (Partial<CalendarState>, optional): Initial state override
|
|
133
|
+
- `onStateChange` (function, optional): Callback when state changes
|
|
134
|
+
|
|
135
|
+
**Returns:** `Calendar<TItem, TOptions>` - Calendar instance with conditional methods
|
|
136
|
+
|
|
137
|
+
**Example:**
|
|
83
138
|
```tsx
|
|
84
|
-
import { useCalendar, createCalendarViews } from '@gobrand/react-calendar';
|
|
139
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
140
|
+
|
|
141
|
+
type Event = {
|
|
142
|
+
id: string;
|
|
143
|
+
title: string;
|
|
144
|
+
start: Temporal.ZonedDateTime;
|
|
145
|
+
end?: Temporal.ZonedDateTime;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const accessor = createCalendarAccessor<Event>({
|
|
149
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
150
|
+
getStart: (event) => event.start,
|
|
151
|
+
getEnd: (event) => event.end,
|
|
152
|
+
});
|
|
85
153
|
|
|
86
154
|
const calendar = useCalendar({
|
|
87
155
|
data: events,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
156
|
+
timeZone: 'America/New_York',
|
|
157
|
+
views: createCalendarViews<Event>()({
|
|
158
|
+
month: { accessor },
|
|
159
|
+
week: { startHour: 8, endHour: 18, accessor },
|
|
160
|
+
day: { startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
92
161
|
}),
|
|
93
162
|
});
|
|
94
163
|
|
|
95
|
-
//
|
|
96
|
-
calendar.
|
|
164
|
+
// Type-safe methods - only available if view is configured
|
|
165
|
+
calendar.getMonth(); // ✓ Available (month view configured)
|
|
166
|
+
calendar.getWeek(); // ✓ Available (week view configured)
|
|
167
|
+
calendar.getDay(); // ✓ Available (day view configured)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Core Calendar Methods
|
|
171
|
+
|
|
172
|
+
#### View Methods
|
|
173
|
+
|
|
174
|
+
##### `getMonth()`
|
|
175
|
+
|
|
176
|
+
Get the current month view. Only available if month view is configured.
|
|
177
|
+
|
|
178
|
+
**Returns:** `CalendarMonth<TItem>` - Month grid with weeks and days
|
|
179
|
+
- `weeks` - Array of weeks, each containing 7 days
|
|
180
|
+
- `month` - Temporal.PlainYearMonth for the current month
|
|
181
|
+
|
|
182
|
+
**Example:**
|
|
183
|
+
```tsx
|
|
184
|
+
const month = calendar.getMonth();
|
|
185
|
+
|
|
186
|
+
month.weeks.forEach(week => {
|
|
187
|
+
week.forEach(day => {
|
|
188
|
+
console.log(day.date, day.items, day.isToday, day.isCurrentMonth);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
##### `getWeek()`
|
|
194
|
+
|
|
195
|
+
Get the current week view. Only available if week view is configured.
|
|
196
|
+
|
|
197
|
+
**Returns:** `CalendarWeekView<TItem>` - Week with days and optional time slots
|
|
198
|
+
- `days` - Array of 7 days in the week
|
|
199
|
+
- `weekStart` - Temporal.PlainDate for the first day
|
|
200
|
+
- `weekEnd` - Temporal.PlainDate for the last day
|
|
201
|
+
|
|
202
|
+
**Example:**
|
|
203
|
+
```tsx
|
|
204
|
+
const week = calendar.getWeek();
|
|
97
205
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
206
|
+
week.days.forEach(day => {
|
|
207
|
+
console.log(day.date, day.items);
|
|
208
|
+
day.timeSlots?.forEach(slot => {
|
|
209
|
+
console.log(slot.hour, slot.minute, slot.items);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
102
212
|
```
|
|
103
213
|
|
|
104
|
-
|
|
214
|
+
##### `getDay()`
|
|
105
215
|
|
|
106
|
-
|
|
216
|
+
Get the current day view. Only available if day view is configured.
|
|
107
217
|
|
|
108
|
-
|
|
218
|
+
**Returns:** `CalendarDayView<TItem>` - Day with time slots
|
|
219
|
+
- `date` - Temporal.PlainDate for the day
|
|
220
|
+
- `isToday` - Boolean indicating if this is today
|
|
221
|
+
- `timeSlots` - Array of time slots with items
|
|
222
|
+
- `items` - All items for this day
|
|
109
223
|
|
|
110
|
-
**
|
|
224
|
+
**Example:**
|
|
111
225
|
```tsx
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
226
|
+
const day = calendar.getDay();
|
|
227
|
+
|
|
228
|
+
console.log(day.date, day.isToday);
|
|
229
|
+
day.timeSlots.forEach(slot => {
|
|
230
|
+
console.log(`${slot.hour}:${slot.minute}`, slot.items);
|
|
231
|
+
});
|
|
119
232
|
```
|
|
120
233
|
|
|
121
|
-
|
|
234
|
+
#### Navigation Methods
|
|
122
235
|
|
|
123
|
-
|
|
124
|
-
- `getState()` - Get current state
|
|
125
|
-
- `setState(updater)` - Update state
|
|
126
|
-
- `goToToday()` - Navigate to today
|
|
127
|
-
- `goToDate(date)` - Navigate to specific date
|
|
128
|
-
- `getTitle(view)` - Get formatted title for view
|
|
129
|
-
- `dateRange` - Current date range (for data fetching)
|
|
130
|
-
- `currentView` - Current view name
|
|
131
|
-
- `setCurrentView(view)` - Switch views
|
|
236
|
+
##### Month Navigation
|
|
132
237
|
|
|
133
|
-
|
|
238
|
+
```tsx
|
|
239
|
+
calendar.nextMonth(); // Go to next month
|
|
240
|
+
calendar.previousMonth(); // Go to previous month
|
|
241
|
+
calendar.goToMonth(2025, 6); // Go to specific month (year, month)
|
|
242
|
+
```
|
|
134
243
|
|
|
135
|
-
|
|
136
|
-
- `getMonth()` - Returns `{ month: PlainYearMonth, weeks: CalendarWeek<T>[] }`
|
|
137
|
-
- `nextMonth()` / `previousMonth()`
|
|
138
|
-
- `goToMonth(year, month)`
|
|
244
|
+
##### Week Navigation
|
|
139
245
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
246
|
+
```tsx
|
|
247
|
+
calendar.nextWeek(); // Go to next week
|
|
248
|
+
calendar.previousWeek(); // Go to previous week
|
|
249
|
+
```
|
|
143
250
|
|
|
144
|
-
|
|
145
|
-
- `getDay()` - Returns `{ date, timeSlots: { hour, minute, items: T[] }[] }`
|
|
146
|
-
- `nextDay()` / `previousDay()`
|
|
251
|
+
##### Day Navigation
|
|
147
252
|
|
|
148
|
-
|
|
253
|
+
```tsx
|
|
254
|
+
calendar.nextDay(); // Go to next day
|
|
255
|
+
calendar.previousDay(); // Go to previous day
|
|
256
|
+
```
|
|
149
257
|
|
|
150
|
-
|
|
258
|
+
##### Universal Navigation
|
|
151
259
|
|
|
152
260
|
```tsx
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
261
|
+
calendar.goToToday(); // Go to today (works for all views)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### View Management
|
|
265
|
+
|
|
266
|
+
##### `setCurrentView(view)`
|
|
267
|
+
|
|
268
|
+
Switch between configured views.
|
|
269
|
+
|
|
270
|
+
**Parameters:**
|
|
271
|
+
- `view` (string): One of the configured view names ('month' | 'week' | 'day')
|
|
272
|
+
|
|
273
|
+
**Example:**
|
|
274
|
+
```tsx
|
|
275
|
+
calendar.setCurrentView('week'); // Switch to week view
|
|
276
|
+
calendar.setCurrentView('month'); // Switch to month view
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
##### `getTitle(view?, locales?, options?)`
|
|
280
|
+
|
|
281
|
+
Get a formatted title for the current view or a specific view.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
- `view` (string, optional): View name (defaults to current view)
|
|
285
|
+
- `locales` (string | string[], optional): Locale(s) for formatting
|
|
286
|
+
- `options` (Intl.DateTimeFormatOptions, optional): Formatting options
|
|
287
|
+
|
|
288
|
+
**Returns:** `string` - Formatted title
|
|
289
|
+
|
|
290
|
+
**Example:**
|
|
291
|
+
```tsx
|
|
292
|
+
calendar.getTitle('month'); // "January 2025"
|
|
293
|
+
calendar.getTitle('month', 'es-ES'); // "enero de 2025"
|
|
294
|
+
calendar.getTitle('week'); // "Jan 20 – 26, 2025"
|
|
295
|
+
calendar.getTitle('day', 'en-US', {
|
|
296
|
+
weekday: 'long',
|
|
297
|
+
year: 'numeric',
|
|
298
|
+
month: 'long',
|
|
299
|
+
day: 'numeric'
|
|
300
|
+
}); // "Monday, January 20, 2025"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### State Management
|
|
304
|
+
|
|
305
|
+
##### `getState()`
|
|
306
|
+
|
|
307
|
+
Get the current calendar state.
|
|
308
|
+
|
|
309
|
+
**Returns:** `CalendarState` - Current state object
|
|
310
|
+
- `currentView` - Active view name
|
|
311
|
+
- `referenceDate` - Current reference date (PlainDate)
|
|
312
|
+
|
|
313
|
+
##### `setState(updater)`
|
|
314
|
+
|
|
315
|
+
Update the calendar state.
|
|
316
|
+
|
|
317
|
+
**Parameters:**
|
|
318
|
+
- `updater` (function | object): State update function or partial state object
|
|
319
|
+
|
|
320
|
+
**Example:**
|
|
321
|
+
```tsx
|
|
322
|
+
// Function updater
|
|
323
|
+
calendar.setState(state => ({
|
|
324
|
+
...state,
|
|
325
|
+
referenceDate: Temporal.PlainDate.from('2025-12-25')
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
// Object updater
|
|
329
|
+
calendar.setState({ currentView: 'week' });
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Accessor Pattern
|
|
333
|
+
|
|
334
|
+
#### `createCalendarAccessor(accessor)`
|
|
335
|
+
|
|
336
|
+
Create a type-safe accessor for mapping your data to calendar dates. This is a type-identity function for TypeScript inference.
|
|
337
|
+
|
|
338
|
+
**Parameters:**
|
|
339
|
+
- `accessor` (CalendarAccessor<TItem>): Accessor configuration
|
|
340
|
+
- `getDate` (required): Extract PlainDate from item
|
|
341
|
+
- `getStart` (optional): Extract ZonedDateTime start time
|
|
342
|
+
- `getEnd` (optional): Extract ZonedDateTime end time
|
|
343
|
+
|
|
344
|
+
**Returns:** Same accessor object with proper types
|
|
345
|
+
|
|
346
|
+
**Example:**
|
|
347
|
+
```tsx
|
|
348
|
+
// Simple date-based items (tasks, posts)
|
|
349
|
+
type Task = {
|
|
350
|
+
id: string;
|
|
351
|
+
name: string;
|
|
352
|
+
dueDate: Temporal.PlainDate;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const taskAccessor = createCalendarAccessor<Task>({
|
|
356
|
+
getDate: (task) => task.dueDate,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Time-based items with start time (events, appointments)
|
|
360
|
+
type Event = {
|
|
361
|
+
id: string;
|
|
362
|
+
title: string;
|
|
363
|
+
start: Temporal.ZonedDateTime;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const eventAccessor = createCalendarAccessor<Event>({
|
|
367
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
368
|
+
getStart: (event) => event.start,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Items with start and end times (meetings, bookings)
|
|
372
|
+
type Meeting = {
|
|
373
|
+
id: string;
|
|
374
|
+
title: string;
|
|
375
|
+
start: Temporal.ZonedDateTime;
|
|
376
|
+
end: Temporal.ZonedDateTime;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const meetingAccessor = createCalendarAccessor<Meeting>({
|
|
380
|
+
getDate: (meeting) => meeting.start.toPlainDate(),
|
|
381
|
+
getStart: (meeting) => meeting.start,
|
|
382
|
+
getEnd: (meeting) => meeting.end,
|
|
171
383
|
});
|
|
172
384
|
```
|
|
173
385
|
|
|
174
|
-
###
|
|
386
|
+
### View Configuration
|
|
175
387
|
|
|
176
|
-
|
|
388
|
+
#### `createCalendarViews()`
|
|
177
389
|
|
|
390
|
+
Create type-safe view configurations. This is a curried function that requires a type parameter.
|
|
391
|
+
|
|
392
|
+
**Usage:**
|
|
178
393
|
```tsx
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
394
|
+
const views = createCalendarViews<TItem>()({
|
|
395
|
+
month?: { ... },
|
|
396
|
+
week?: { ... },
|
|
397
|
+
day?: { ... }
|
|
183
398
|
});
|
|
184
399
|
```
|
|
185
400
|
|
|
186
|
-
|
|
401
|
+
#### Month View Options
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
type MonthViewOptions<TItem> = {
|
|
405
|
+
accessor: CalendarAccessor<TItem>;
|
|
406
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Sunday, 1 = Monday, etc.
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Week View Options
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
type WeekViewOptions<TItem> = {
|
|
414
|
+
accessor: CalendarAccessor<TItem>;
|
|
415
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
416
|
+
startHour?: number; // Default: 0
|
|
417
|
+
endHour?: number; // Default: 24
|
|
418
|
+
slotDuration?: number; // Minutes per slot, default: 60
|
|
419
|
+
};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### Day View Options
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
type DayViewOptions<TItem> = {
|
|
426
|
+
accessor: CalendarAccessor<TItem>;
|
|
427
|
+
startHour?: number; // Default: 0
|
|
428
|
+
endHour?: number; // Default: 24
|
|
429
|
+
slotDuration?: number; // Minutes per slot, default: 60
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Core Utilities (Re-exported)
|
|
187
434
|
|
|
188
|
-
All utilities from
|
|
435
|
+
All utilities from [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) are re-exported for convenience. For detailed documentation on these functions, see the [core package documentation](https://www.npmjs.com/package/@gobrand/calendar-core).
|
|
436
|
+
|
|
437
|
+
**Building Functions:**
|
|
438
|
+
- `buildMonth(year, month, options?)` - Build month grid
|
|
439
|
+
- `buildWeek(date, options?)` - Build week view
|
|
440
|
+
- `buildDay(date, options?)` - Build day view with time slots
|
|
189
441
|
|
|
190
442
|
**Formatting:**
|
|
191
|
-
- `getWeekdays(weekStartsOn?)` - Localized weekday names
|
|
443
|
+
- `getWeekdays(weekStartsOn?, locale?, format?)` - Localized weekday names
|
|
192
444
|
- `getMonthName(month, locale?)` - Localized month name
|
|
193
445
|
- `formatTime(time, locale?)` - Format PlainTime
|
|
194
446
|
|
|
195
|
-
**Navigation:**
|
|
196
|
-
- `nextMonth(month)` / `previousMonth(month)`
|
|
197
|
-
- `nextWeek(date)` / `previousWeek(date)`
|
|
198
|
-
- `nextDay(date)` / `previousDay(date)`
|
|
199
|
-
- `goToToday()` - Get current PlainDate
|
|
200
|
-
|
|
201
447
|
**Timezone:**
|
|
202
448
|
- `getMonthRange(timeZone?, weekStartsOn?)` - Week-aligned month range
|
|
203
449
|
- `getWeekRange(timeZone?, weekStartsOn?)` - Current week range
|
|
@@ -206,98 +452,417 @@ All utilities from `@gobrand/calendar-core` are re-exported:
|
|
|
206
452
|
- `convertToTimezone(zdt, timeZone)` - Convert between timezones
|
|
207
453
|
- `createZonedDateTime(date, time, timeZone)` - Create ZonedDateTime
|
|
208
454
|
|
|
209
|
-
|
|
210
|
-
- `getEventPosition(start, end, startHour, endHour, slotDuration)` - Calculate event positioning for time-based grids
|
|
455
|
+
For detailed documentation and examples, see [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core).
|
|
211
456
|
|
|
212
|
-
##
|
|
457
|
+
## Real World Examples
|
|
213
458
|
|
|
214
|
-
###
|
|
459
|
+
### Example 1: Fetching Calendar Data with useQuery and Date-Range Pagination
|
|
460
|
+
|
|
461
|
+
This is the most common real-world pattern: fetching data from an API based on the visible calendar date range. As users navigate the calendar (month/week/day), new data is automatically fetched for that time period.
|
|
215
462
|
|
|
216
463
|
```tsx
|
|
217
|
-
{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
464
|
+
import { useQuery } from '@tanstack/react-query';
|
|
465
|
+
import { toZonedTime, now } from '@gobrand/tiempo';
|
|
466
|
+
import {
|
|
467
|
+
useCalendar,
|
|
468
|
+
createCalendarViews,
|
|
469
|
+
createCalendarAccessor,
|
|
470
|
+
type DateRange,
|
|
471
|
+
getMonthDateRange,
|
|
472
|
+
} from '@gobrand/react-calendar';
|
|
473
|
+
import { useState } from 'react';
|
|
474
|
+
|
|
475
|
+
type Post = {
|
|
476
|
+
id: string;
|
|
477
|
+
title: string;
|
|
478
|
+
publishedAt: string; // UTC ISO 8601 string from API
|
|
479
|
+
status: 'draft' | 'scheduled' | 'published';
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
type PostWithDateTime = Post & {
|
|
483
|
+
zonedDateTime: Temporal.ZonedDateTime;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const TIME_ZONE = 'America/New_York';
|
|
487
|
+
|
|
488
|
+
function PostCalendar() {
|
|
489
|
+
// Track the current visible date range
|
|
490
|
+
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
|
491
|
+
getMonthDateRange(now(TIME_ZONE).toPlainDate(), TIME_ZONE)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// Fetch posts for the visible date range
|
|
495
|
+
const { data: posts = [], isLoading } = useQuery({
|
|
496
|
+
queryKey: ['posts', dateRange?.start.toString(), dateRange?.end.toString()],
|
|
497
|
+
queryFn: async () => {
|
|
498
|
+
if (!dateRange) return [];
|
|
499
|
+
|
|
500
|
+
// Convert date range to UTC ISO strings for API
|
|
501
|
+
const filters = {
|
|
502
|
+
dateRange: {
|
|
503
|
+
start: dateRange.start.toInstant().toString(),
|
|
504
|
+
end: dateRange.end.toInstant().toString(),
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
return getPosts(filters);
|
|
509
|
+
},
|
|
510
|
+
// Convert UTC strings to ZonedDateTime for calendar
|
|
511
|
+
select: (posts) =>
|
|
512
|
+
posts.map((post) => ({
|
|
513
|
+
...post,
|
|
514
|
+
zonedDateTime: toZonedTime(post.publishedAt, TIME_ZONE),
|
|
515
|
+
})),
|
|
516
|
+
enabled: !!dateRange,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const calendar = useCalendar({
|
|
520
|
+
data: posts,
|
|
521
|
+
timeZone: TIME_ZONE,
|
|
522
|
+
views: createCalendarViews<PostWithDateTime>()({
|
|
523
|
+
month: {
|
|
524
|
+
accessor: createCalendarAccessor({
|
|
525
|
+
getDate: (post) => post.zonedDateTime.toPlainDate(),
|
|
526
|
+
getStart: (post) => post.zonedDateTime,
|
|
527
|
+
}),
|
|
528
|
+
},
|
|
529
|
+
}),
|
|
530
|
+
// Sync date range when calendar navigation changes
|
|
531
|
+
onStateChange: (updater) => {
|
|
532
|
+
const newState =
|
|
533
|
+
typeof updater === 'function' ? updater(calendar.getState()) : updater;
|
|
534
|
+
setDateRange(newState.dateRange);
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const month = calendar.getMonth();
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<div>
|
|
542
|
+
<header>
|
|
543
|
+
<button onClick={calendar.previousMonth}>←</button>
|
|
544
|
+
<h2>{calendar.getTitle('month')}</h2>
|
|
545
|
+
<button onClick={calendar.nextMonth}>→</button>
|
|
546
|
+
{isLoading && <span>Loading...</span>}
|
|
547
|
+
</header>
|
|
548
|
+
|
|
549
|
+
<div className="calendar-grid">
|
|
550
|
+
{month.weeks.flat().map((day) => (
|
|
551
|
+
<div key={day.date.toString()}>
|
|
552
|
+
<div>{day.date.day}</div>
|
|
553
|
+
{day.items.map((post) => (
|
|
554
|
+
<div key={post.id}>{post.title}</div>
|
|
555
|
+
))}
|
|
556
|
+
</div>
|
|
557
|
+
))}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
222
561
|
}
|
|
223
562
|
```
|
|
224
563
|
|
|
225
|
-
|
|
564
|
+
**Key points:**
|
|
565
|
+
- `dateRange` state tracks the currently visible calendar period
|
|
566
|
+
- `useQuery` automatically refetches when `dateRange` changes (navigation)
|
|
567
|
+
- Date range is converted to UTC ISO strings for the API request
|
|
568
|
+
- `select` transforms API responses (UTC ISO strings) to `ZonedDateTime` for the calendar
|
|
569
|
+
- `onStateChange` syncs calendar navigation with the date range state
|
|
570
|
+
- This pattern works for infinite scroll, cursor-based pagination, or any date-range filtering
|
|
226
571
|
|
|
227
|
-
|
|
228
|
-
CalendarDay<T>[] // Array of 7 days
|
|
229
|
-
```
|
|
572
|
+
### Example 2: Event Calendar with Multi-View Support
|
|
230
573
|
|
|
231
|
-
|
|
574
|
+
A complete event calendar with month, week, and day views, timezone support, and type-safe view switching.
|
|
232
575
|
|
|
233
576
|
```tsx
|
|
234
|
-
{
|
|
235
|
-
|
|
236
|
-
|
|
577
|
+
import {
|
|
578
|
+
useCalendar,
|
|
579
|
+
createCalendarViews,
|
|
580
|
+
createCalendarAccessor,
|
|
581
|
+
getWeekdays
|
|
582
|
+
} from '@gobrand/react-calendar';
|
|
583
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
584
|
+
|
|
585
|
+
type Event = {
|
|
586
|
+
id: string;
|
|
587
|
+
title: string;
|
|
588
|
+
description?: string;
|
|
589
|
+
start: Temporal.ZonedDateTime;
|
|
590
|
+
end: Temporal.ZonedDateTime;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const events: Event[] = [
|
|
594
|
+
{
|
|
595
|
+
id: '1',
|
|
596
|
+
title: 'Team Standup',
|
|
597
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T09:00:00-05:00[America/New_York]'),
|
|
598
|
+
end: Temporal.ZonedDateTime.from('2025-01-20T09:30:00-05:00[America/New_York]')
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
id: '2',
|
|
602
|
+
title: 'Client Meeting',
|
|
603
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T14:00:00-05:00[America/New_York]'),
|
|
604
|
+
end: Temporal.ZonedDateTime.from('2025-01-20T15:00:00-05:00[America/New_York]')
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
|
|
608
|
+
const accessor = createCalendarAccessor<Event>({
|
|
609
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
610
|
+
getStart: (event) => event.start,
|
|
611
|
+
getEnd: (event) => event.end,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
function EventCalendar() {
|
|
615
|
+
const calendar = useCalendar({
|
|
616
|
+
data: events,
|
|
617
|
+
timeZone: 'America/New_York',
|
|
618
|
+
views: createCalendarViews<Event>()({
|
|
619
|
+
month: { weekStartsOn: 0, accessor },
|
|
620
|
+
week: { weekStartsOn: 0, startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
621
|
+
day: { startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
622
|
+
}),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const currentView = calendar.getState().currentView;
|
|
626
|
+
|
|
627
|
+
return (
|
|
628
|
+
<div className="calendar">
|
|
629
|
+
{/* Header with view switcher */}
|
|
630
|
+
<header>
|
|
631
|
+
<div className="view-buttons">
|
|
632
|
+
<button
|
|
633
|
+
onClick={() => calendar.setCurrentView('month')}
|
|
634
|
+
className={currentView === 'month' ? 'active' : ''}
|
|
635
|
+
>
|
|
636
|
+
Month
|
|
637
|
+
</button>
|
|
638
|
+
<button
|
|
639
|
+
onClick={() => calendar.setCurrentView('week')}
|
|
640
|
+
className={currentView === 'week' ? 'active' : ''}
|
|
641
|
+
>
|
|
642
|
+
Week
|
|
643
|
+
</button>
|
|
644
|
+
<button
|
|
645
|
+
onClick={() => calendar.setCurrentView('day')}
|
|
646
|
+
className={currentView === 'day' ? 'active' : ''}
|
|
647
|
+
>
|
|
648
|
+
Day
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<h2>{calendar.getTitle()}</h2>
|
|
653
|
+
|
|
654
|
+
<div className="nav-buttons">
|
|
655
|
+
{currentView === 'month' && (
|
|
656
|
+
<>
|
|
657
|
+
<button onClick={calendar.previousMonth}>←</button>
|
|
658
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
659
|
+
<button onClick={calendar.nextMonth}>→</button>
|
|
660
|
+
</>
|
|
661
|
+
)}
|
|
662
|
+
{currentView === 'week' && (
|
|
663
|
+
<>
|
|
664
|
+
<button onClick={calendar.previousWeek}>←</button>
|
|
665
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
666
|
+
<button onClick={calendar.nextWeek}>→</button>
|
|
667
|
+
</>
|
|
668
|
+
)}
|
|
669
|
+
{currentView === 'day' && (
|
|
670
|
+
<>
|
|
671
|
+
<button onClick={calendar.previousDay}>←</button>
|
|
672
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
673
|
+
<button onClick={calendar.nextDay}>→</button>
|
|
674
|
+
</>
|
|
675
|
+
)}
|
|
676
|
+
</div>
|
|
677
|
+
</header>
|
|
678
|
+
|
|
679
|
+
{/* Month View */}
|
|
680
|
+
{currentView === 'month' && (
|
|
681
|
+
<div className="month-view">
|
|
682
|
+
<div className="weekday-headers">
|
|
683
|
+
{getWeekdays(0).map(day => (
|
|
684
|
+
<div key={day} className="weekday">{day}</div>
|
|
685
|
+
))}
|
|
686
|
+
</div>
|
|
687
|
+
<div className="month-grid">
|
|
688
|
+
{calendar.getMonth().weeks.flat().map(day => (
|
|
689
|
+
<div
|
|
690
|
+
key={day.date.toString()}
|
|
691
|
+
className={`day ${!day.isCurrentMonth ? 'other-month' : ''} ${day.isToday ? 'today' : ''}`}
|
|
692
|
+
>
|
|
693
|
+
<div className="day-number">{day.date.day}</div>
|
|
694
|
+
<div className="events">
|
|
695
|
+
{day.items.map(event => (
|
|
696
|
+
<div key={event.id} className="event">
|
|
697
|
+
{event.title}
|
|
698
|
+
</div>
|
|
699
|
+
))}
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
))}
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
)}
|
|
706
|
+
|
|
707
|
+
{/* Week View */}
|
|
708
|
+
{currentView === 'week' && (
|
|
709
|
+
<div className="week-view">
|
|
710
|
+
<div className="weekday-headers">
|
|
711
|
+
{calendar.getWeek().days.map(day => (
|
|
712
|
+
<div key={day.date.toString()} className="weekday">
|
|
713
|
+
<div>{day.date.toLocaleString('en-US', { weekday: 'short' })}</div>
|
|
714
|
+
<div className={day.isToday ? 'today' : ''}>{day.date.day}</div>
|
|
715
|
+
</div>
|
|
716
|
+
))}
|
|
717
|
+
</div>
|
|
718
|
+
<div className="week-grid">
|
|
719
|
+
{calendar.getWeek().days.map(day => (
|
|
720
|
+
<div key={day.date.toString()} className="day-column">
|
|
721
|
+
{day.timeSlots?.map((slot, i) => (
|
|
722
|
+
<div key={i} className="time-slot">
|
|
723
|
+
{slot.items.map(event => (
|
|
724
|
+
<div key={event.id} className="event">
|
|
725
|
+
{event.title}
|
|
726
|
+
</div>
|
|
727
|
+
))}
|
|
728
|
+
</div>
|
|
729
|
+
))}
|
|
730
|
+
</div>
|
|
731
|
+
))}
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
{/* Day View */}
|
|
737
|
+
{currentView === 'day' && (
|
|
738
|
+
<div className="day-view">
|
|
739
|
+
{calendar.getDay().timeSlots.map((slot, i) => (
|
|
740
|
+
<div key={i} className="time-slot">
|
|
741
|
+
<div className="time">{slot.hour}:{String(slot.minute).padStart(2, '0')}</div>
|
|
742
|
+
<div className="slot-events">
|
|
743
|
+
{slot.items.map(event => (
|
|
744
|
+
<div key={event.id} className="event">
|
|
745
|
+
<strong>{event.title}</strong>
|
|
746
|
+
{event.description && <p>{event.description}</p>}
|
|
747
|
+
</div>
|
|
748
|
+
))}
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
))}
|
|
752
|
+
</div>
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
);
|
|
237
756
|
}
|
|
238
757
|
```
|
|
239
758
|
|
|
240
|
-
###
|
|
759
|
+
### Example 2: Task Due Date Calendar
|
|
760
|
+
|
|
761
|
+
Simple calendar showing task due dates without time information.
|
|
241
762
|
|
|
242
763
|
```tsx
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
764
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
765
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
766
|
+
|
|
767
|
+
type Task = {
|
|
768
|
+
id: string;
|
|
769
|
+
title: string;
|
|
770
|
+
dueDate: Temporal.PlainDate;
|
|
771
|
+
completed: boolean;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const tasks: Task[] = [
|
|
775
|
+
{ id: '1', title: 'Review PR #42', dueDate: Temporal.PlainDate.from('2025-01-20'), completed: false },
|
|
776
|
+
{ id: '2', title: 'Write documentation', dueDate: Temporal.PlainDate.from('2025-01-22'), completed: false },
|
|
777
|
+
{ id: '3', title: 'Deploy to production', dueDate: Temporal.PlainDate.from('2025-01-25'), completed: true },
|
|
778
|
+
];
|
|
779
|
+
|
|
780
|
+
function TaskCalendar() {
|
|
781
|
+
const calendar = useCalendar({
|
|
782
|
+
data: tasks,
|
|
783
|
+
views: createCalendarViews<Task>()({
|
|
784
|
+
month: {
|
|
785
|
+
accessor: createCalendarAccessor({
|
|
786
|
+
getDate: (task) => task.dueDate,
|
|
787
|
+
})
|
|
788
|
+
},
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const month = calendar.getMonth();
|
|
793
|
+
|
|
794
|
+
return (
|
|
795
|
+
<div>
|
|
796
|
+
<header>
|
|
797
|
+
<button onClick={calendar.previousMonth}>Previous</button>
|
|
798
|
+
<h2>{calendar.getTitle('month')}</h2>
|
|
799
|
+
<button onClick={calendar.nextMonth}>Next</button>
|
|
800
|
+
</header>
|
|
801
|
+
|
|
802
|
+
<div className="calendar-grid">
|
|
803
|
+
{month.weeks.flat().map(day => (
|
|
804
|
+
<div
|
|
805
|
+
key={day.date.toString()}
|
|
806
|
+
className={!day.isCurrentMonth ? 'dimmed' : ''}
|
|
807
|
+
>
|
|
808
|
+
<div>{day.date.day}</div>
|
|
809
|
+
{day.items.map(task => (
|
|
810
|
+
<div
|
|
811
|
+
key={task.id}
|
|
812
|
+
className={task.completed ? 'completed' : 'pending'}
|
|
813
|
+
>
|
|
814
|
+
{task.title}
|
|
815
|
+
</div>
|
|
816
|
+
))}
|
|
817
|
+
</div>
|
|
818
|
+
))}
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
);
|
|
247
822
|
}
|
|
248
823
|
```
|
|
249
824
|
|
|
250
|
-
|
|
825
|
+
> **For vanilla JavaScript examples:** Check out [@gobrand/calendar-core documentation](https://www.npmjs.com/package/@gobrand/calendar-core) for framework-agnostic usage examples.
|
|
251
826
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
items: T[]; // Items overlapping this time slot
|
|
259
|
-
}[];
|
|
260
|
-
}
|
|
827
|
+
## Browser Support
|
|
828
|
+
|
|
829
|
+
The Temporal API is a Stage 3 TC39 proposal. The polyfill `@js-temporal/polyfill` is included as a dependency, ensuring compatibility across all modern browsers.
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
261
833
|
```
|
|
262
834
|
|
|
263
|
-
|
|
835
|
+
**Requirements:**
|
|
836
|
+
- React 18+ or React 19+
|
|
837
|
+
- Modern browsers with ES2015+ support
|
|
264
838
|
|
|
265
|
-
|
|
839
|
+
## Development
|
|
266
840
|
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
data: events,
|
|
271
|
-
views: createCalendarViews({ month: { ... } }),
|
|
272
|
-
});
|
|
841
|
+
```bash
|
|
842
|
+
# Install dependencies
|
|
843
|
+
pnpm install
|
|
273
844
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
calendar.getWeek(); // ❌ Type error - week view not configured
|
|
845
|
+
# Build all packages
|
|
846
|
+
pnpm build
|
|
277
847
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
data: events,
|
|
281
|
-
views: createCalendarViews({
|
|
282
|
-
month: { ... },
|
|
283
|
-
week: { ... },
|
|
284
|
-
day: { ... },
|
|
285
|
-
}),
|
|
286
|
-
});
|
|
848
|
+
# Run tests
|
|
849
|
+
pnpm test --run
|
|
287
850
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
851
|
+
# Type check
|
|
852
|
+
pnpm typecheck
|
|
853
|
+
|
|
854
|
+
# Release new version
|
|
855
|
+
pnpm release <patch|minor|major>
|
|
291
856
|
```
|
|
292
857
|
|
|
293
|
-
##
|
|
858
|
+
## Contributing
|
|
294
859
|
|
|
295
|
-
|
|
296
|
-
- **Month view**: [PostMonthlyView.tsx](../../apps/demo/src/components/PostMonthlyView.tsx)
|
|
297
|
-
- **Week view**: [PostWeeklyView.tsx](../../apps/demo/src/components/PostWeeklyView.tsx)
|
|
298
|
-
- **Day view**: [PostDailyView.tsx](../../apps/demo/src/components/PostDailyView.tsx)
|
|
299
|
-
- **Custom view**: [PostAgendaView.tsx](../../apps/demo/src/components/PostAgendaView.tsx)
|
|
860
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
300
861
|
|
|
301
862
|
## License
|
|
302
863
|
|
|
303
864
|
MIT
|
|
865
|
+
|
|
866
|
+
## Built by Go Brand
|
|
867
|
+
|
|
868
|
+
temporal-calendar is built and maintained by [Go Brand](https://gobrand.app) - a modern social media management platform.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobrand/react-calendar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "React hooks and components for building calendars using the Temporal API",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@js-temporal/polyfill": "^0.5.1",
|
|
42
|
-
"@gobrand/calendar-core": "^0.0.
|
|
42
|
+
"@gobrand/calendar-core": "^0.0.10"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0"
|