@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.
Files changed (2) hide show
  1. package/README.md +717 -152
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,24 +1,54 @@
1
1
  # @gobrand/react-calendar
2
2
 
3
- React hook for building calendars with the Temporal API.
3
+ [![npm version](https://img.shields.io/npm/v/@gobrand/react-calendar.svg)](https://www.npmjs.com/package/@gobrand/react-calendar)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ## Philosophy
12
-
13
- - **Data-agnostic**: Works with any data type (events, tasks, posts, etc.)
14
- - **Type-safe**: Full TypeScript support with TanStack-style type inference
15
- - **Zero abstractions**: Direct use of Temporal API primitives
16
- - **Minimal API surface**: Just a hook and utility functions
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
- // Define how to extract dates from your data
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: { weekStartsOn: 1, accessor },
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
- ## Multi-View Calendar
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
- views: createCalendarViews({
89
- month: { weekStartsOn: 1, accessor: monthAccessor },
90
- week: { weekStartsOn: 1, startHour: 8, endHour: 18, accessor: weekAccessor },
91
- day: { startHour: 8, endHour: 18, slotDuration: 30, accessor: dayAccessor },
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
- // Switch views
96
- calendar.setCurrentView('week');
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
- // Get current view data
99
- const month = calendar.getMonth(); // Type-safe! Only available if month view configured
100
- const week = calendar.getWeek(); // Type-safe! Only available if week view configured
101
- const day = calendar.getDay(); // Type-safe! Only available if day view configured
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
- ## API
214
+ ##### `getDay()`
105
215
 
106
- ### `useCalendar<TOptions>(options)`
216
+ Get the current day view. Only available if day view is configured.
107
217
 
108
- Main hook for calendar state management.
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
- **Options:**
224
+ **Example:**
111
225
  ```tsx
112
- {
113
- data: T[]; // Your data array
114
- views: CalendarViewOptions<T>; // View configurations
115
- timeZone?: string; // IANA timezone (default: system)
116
- state?: CalendarState; // External state (for controlled mode)
117
- onStateChange?: (state) => void; // State change callback
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
- **Returns:** `Calendar<T, TOptions>` with methods based on configured views
234
+ #### Navigation Methods
122
235
 
123
- **Common methods:**
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
- **View-specific methods** (conditionally available based on configured views):
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
- **Month view:**
136
- - `getMonth()` - Returns `{ month: PlainYearMonth, weeks: CalendarWeek<T>[] }`
137
- - `nextMonth()` / `previousMonth()`
138
- - `goToMonth(year, month)`
244
+ ##### Week Navigation
139
245
 
140
- **Week view:**
141
- - `getWeek()` - Returns `{ weekStart, weekEnd, days: CalendarDay<T>[] }`
142
- - `nextWeek()` / `previousWeek()`
246
+ ```tsx
247
+ calendar.nextWeek(); // Go to next week
248
+ calendar.previousWeek(); // Go to previous week
249
+ ```
143
250
 
144
- **Day view:**
145
- - `getDay()` - Returns `{ date, timeSlots: { hour, minute, items: T[] }[] }`
146
- - `nextDay()` / `previousDay()`
251
+ ##### Day Navigation
147
252
 
148
- ### `createCalendarViews<T>(config)`
253
+ ```tsx
254
+ calendar.nextDay(); // Go to next day
255
+ calendar.previousDay(); // Go to previous day
256
+ ```
149
257
 
150
- Type-safe builder for view configurations.
258
+ ##### Universal Navigation
151
259
 
152
260
  ```tsx
153
- const views = createCalendarViews<Event>({
154
- month: {
155
- weekStartsOn: 1, // 0 (Sun) - 6 (Sat)
156
- accessor: monthAccessor, // CalendarAccessor<Event>
157
- },
158
- week: {
159
- weekStartsOn: 1,
160
- startHour: 8, // 0-23
161
- endHour: 18, // 0-24
162
- slotDuration: 30, // Minutes
163
- accessor: weekAccessor,
164
- },
165
- day: {
166
- startHour: 8,
167
- endHour: 18,
168
- slotDuration: 30,
169
- accessor: dayAccessor,
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
- ### `createCalendarAccessor<T>(config)`
386
+ ### View Configuration
175
387
 
176
- Define how to extract date information from your data.
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 accessor = createCalendarAccessor<Event>({
180
- getDate: (item) => Temporal.PlainDate, // Required
181
- getStart?: (item) => Temporal.ZonedDateTime, // For time-based views
182
- getEnd?: (item) => Temporal.ZonedDateTime, // For time-based views
394
+ const views = createCalendarViews<TItem>()({
395
+ month?: { ... },
396
+ week?: { ... },
397
+ day?: { ... }
183
398
  });
184
399
  ```
185
400
 
186
- ### Utility Functions
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 `@gobrand/calendar-core` are re-exported:
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
- **Layout:**
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
- ## Data Structures
457
+ ## Real World Examples
213
458
 
214
- ### `CalendarDay<T>`
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
- date: Temporal.PlainDate;
219
- isCurrentMonth: boolean;
220
- isToday: boolean;
221
- items: T[]; // Your data filtered to this day
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
- ### `CalendarWeek<T>`
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
- ```tsx
228
- CalendarDay<T>[] // Array of 7 days
229
- ```
572
+ ### Example 2: Event Calendar with Multi-View Support
230
573
 
231
- ### `CalendarMonth<T>`
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
- month: Temporal.PlainYearMonth;
236
- weeks: CalendarWeek<T>[];
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
- ### `CalendarWeekView<T>`
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
- weekStart: Temporal.PlainDate;
245
- weekEnd: Temporal.PlainDate;
246
- days: CalendarDay<T>[];
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
- ### `CalendarDayView<T>`
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
- ```tsx
253
- {
254
- date: Temporal.PlainDate;
255
- timeSlots: {
256
- hour: number;
257
- minute: number;
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
- ## Type Safety
835
+ **Requirements:**
836
+ - React 18+ or React 19+
837
+ - Modern browsers with ES2015+ support
264
838
 
265
- The hook uses TanStack-style type inference to provide conditional types based on your configuration:
839
+ ## Development
266
840
 
267
- ```tsx
268
- // Only month view configured
269
- const calendar = useCalendar({
270
- data: events,
271
- views: createCalendarViews({ month: { ... } }),
272
- });
841
+ ```bash
842
+ # Install dependencies
843
+ pnpm install
273
844
 
274
- calendar.getMonth(); // Available
275
- calendar.nextMonth(); // ✅ Available
276
- calendar.getWeek(); // ❌ Type error - week view not configured
845
+ # Build all packages
846
+ pnpm build
277
847
 
278
- // All views configured
279
- const calendar = useCalendar({
280
- data: events,
281
- views: createCalendarViews({
282
- month: { ... },
283
- week: { ... },
284
- day: { ... },
285
- }),
286
- });
848
+ # Run tests
849
+ pnpm test --run
287
850
 
288
- calendar.getMonth(); // Available
289
- calendar.getWeek(); // ✅ Available
290
- calendar.getDay(); // ✅ Available
851
+ # Type check
852
+ pnpm typecheck
853
+
854
+ # Release new version
855
+ pnpm release <patch|minor|major>
291
856
  ```
292
857
 
293
- ## Examples
858
+ ## Contributing
294
859
 
295
- See the [demo app](../../apps/demo) for complete examples:
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.9",
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.6"
42
+ "@gobrand/calendar-core": "^0.0.10"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^18.0.0 || ^19.0.0"