@gobrand/calendar-core 0.0.9 → 0.0.10
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 +443 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# @gobrand/calendar-core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@gobrand/calendar-core)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Framework-agnostic calendar utilities built with the [Temporal API](https://tc39.es/proposal-temporal/docs/). Simple, composable functions for building month, week, and day views with full timezone support.
|
|
7
|
+
|
|
8
|
+
> **For React users:** Check out [@gobrand/react-calendar](https://www.npmjs.com/package/@gobrand/react-calendar) for a ready-to-use hook with state management built on top of this core library.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @gobrand/calendar-core
|
|
14
|
+
# or
|
|
15
|
+
pnpm add @gobrand/calendar-core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why @gobrand/calendar-core?
|
|
19
|
+
|
|
20
|
+
Building calendars is complex: timezone handling, DST transitions, date arithmetic, and data mapping. **@gobrand/calendar-core** provides pure, framework-agnostic functions for calendar logic:
|
|
21
|
+
|
|
22
|
+
- **🌍 Timezone-aware** - Native timezone support with Temporal API primitives
|
|
23
|
+
- **🎯 Data-agnostic** - Works with any data type through accessor pattern
|
|
24
|
+
- **⚡️ Type-safe** - Full TypeScript support with proper Temporal types
|
|
25
|
+
- **📦 Minimal** - Simple, composable functions with no unnecessary abstractions
|
|
26
|
+
- **🔧 Zero config** - Sensible defaults, customize only what you need
|
|
27
|
+
- **🪄 Framework-agnostic** - Use with React, Vue, Angular, Svelte, or vanilla JavaScript
|
|
28
|
+
|
|
29
|
+
**Key features:**
|
|
30
|
+
- ✅ Built exclusively on Temporal API (no Date objects, no moment.js, no date-fns)
|
|
31
|
+
- ✅ Automatic DST handling and timezone conversions
|
|
32
|
+
- ✅ Calendar-aware arithmetic (leap years, month-end dates)
|
|
33
|
+
- ✅ Flexible accessor pattern for any data structure
|
|
34
|
+
- ✅ Polyfill included for browser compatibility
|
|
35
|
+
|
|
36
|
+
**Perfect for:**
|
|
37
|
+
- Building custom calendar UIs in any framework
|
|
38
|
+
- Server-side calendar generation
|
|
39
|
+
- Event calendars and schedulers
|
|
40
|
+
- Booking systems and appointment managers
|
|
41
|
+
- Task management with due dates
|
|
42
|
+
- Analytics dashboards with date ranges
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { buildMonth, createCalendarAccessor, getWeekdays, getMonthName } from '@gobrand/calendar-core';
|
|
48
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
49
|
+
|
|
50
|
+
type Event = {
|
|
51
|
+
id: string;
|
|
52
|
+
date: Temporal.PlainDate;
|
|
53
|
+
title: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const events: Event[] = [
|
|
57
|
+
{ id: '1', date: Temporal.PlainDate.from('2025-01-20'), title: 'Team Meeting' },
|
|
58
|
+
{ id: '2', date: Temporal.PlainDate.from('2025-01-22'), title: 'Code Review' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const accessor = createCalendarAccessor<Event>({
|
|
62
|
+
getDate: (event) => event.date,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const month = buildMonth(2025, 1, {
|
|
66
|
+
weekStartsOn: 1,
|
|
67
|
+
data: events,
|
|
68
|
+
accessor,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(getMonthName(month.month)); // "January"
|
|
72
|
+
console.log(getWeekdays(1)); // ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
73
|
+
|
|
74
|
+
month.weeks.forEach(week => {
|
|
75
|
+
week.forEach(day => {
|
|
76
|
+
console.log(
|
|
77
|
+
day.date.toString(),
|
|
78
|
+
day.isCurrentMonth,
|
|
79
|
+
day.isToday,
|
|
80
|
+
`${day.items.length} events`
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### Accessor Pattern
|
|
89
|
+
|
|
90
|
+
#### `createCalendarAccessor(accessor)`
|
|
91
|
+
|
|
92
|
+
Create a type-safe accessor for mapping your data to calendar dates. This is a type-identity function for TypeScript inference.
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
- `accessor` (CalendarAccessor<TItem>): Accessor configuration
|
|
96
|
+
- `getDate` (required): Extract PlainDate from item
|
|
97
|
+
- `getStart` (optional): Extract ZonedDateTime start time
|
|
98
|
+
- `getEnd` (optional): Extract ZonedDateTime end time
|
|
99
|
+
|
|
100
|
+
**Returns:** Same accessor object with proper types
|
|
101
|
+
|
|
102
|
+
**Example:**
|
|
103
|
+
```tsx
|
|
104
|
+
// Simple date-based items (tasks, posts)
|
|
105
|
+
type Task = {
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
dueDate: Temporal.PlainDate;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const taskAccessor = createCalendarAccessor<Task>({
|
|
112
|
+
getDate: (task) => task.dueDate,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Time-based items with start time (events, appointments)
|
|
116
|
+
type Event = {
|
|
117
|
+
id: string;
|
|
118
|
+
title: string;
|
|
119
|
+
start: Temporal.ZonedDateTime;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const eventAccessor = createCalendarAccessor<Event>({
|
|
123
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
124
|
+
getStart: (event) => event.start,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Items with start and end times (meetings, bookings)
|
|
128
|
+
type Meeting = {
|
|
129
|
+
id: string;
|
|
130
|
+
title: string;
|
|
131
|
+
start: Temporal.ZonedDateTime;
|
|
132
|
+
end: Temporal.ZonedDateTime;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const meetingAccessor = createCalendarAccessor<Meeting>({
|
|
136
|
+
getDate: (meeting) => meeting.start.toPlainDate(),
|
|
137
|
+
getStart: (meeting) => meeting.start,
|
|
138
|
+
getEnd: (meeting) => meeting.end,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Building Calendars
|
|
143
|
+
|
|
144
|
+
Low-level functions for building calendar grids without state management.
|
|
145
|
+
|
|
146
|
+
#### `buildMonth(year, month, options?)`
|
|
147
|
+
|
|
148
|
+
Build a month grid for any year and month.
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
- `year` (number): Year (e.g., 2025)
|
|
152
|
+
- `month` (number): Month (1-12)
|
|
153
|
+
- `options` (object, optional):
|
|
154
|
+
- `weekStartsOn` (0-6): First day of week
|
|
155
|
+
- `today` (PlainDate): Override today's date
|
|
156
|
+
- `data` (TItem[]): Items to include
|
|
157
|
+
- `accessor` (CalendarAccessor<TItem>): Data accessor
|
|
158
|
+
|
|
159
|
+
**Returns:** `CalendarMonth<TItem>`
|
|
160
|
+
|
|
161
|
+
**Example:**
|
|
162
|
+
```typescript
|
|
163
|
+
import { buildMonth, createCalendarAccessor } from '@gobrand/calendar-core';
|
|
164
|
+
|
|
165
|
+
const month = buildMonth(2025, 1, {
|
|
166
|
+
weekStartsOn: 1,
|
|
167
|
+
data: events,
|
|
168
|
+
accessor: createCalendarAccessor({
|
|
169
|
+
getDate: (event) => event.date
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### `buildWeek(date, options?)`
|
|
175
|
+
|
|
176
|
+
Build a week view for a specific date.
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
- `date` (PlainDate): Any date in the target week
|
|
180
|
+
- `options` (object, optional):
|
|
181
|
+
- `weekStartsOn` (0-6): First day of week
|
|
182
|
+
- `startHour` (number): Start hour for time slots
|
|
183
|
+
- `endHour` (number): End hour for time slots
|
|
184
|
+
- `slotDuration` (number): Minutes per slot
|
|
185
|
+
- `today` (PlainDate): Override today's date
|
|
186
|
+
- `data` (TItem[]): Items to include
|
|
187
|
+
- `accessor` (CalendarAccessor<TItem>): Data accessor
|
|
188
|
+
|
|
189
|
+
**Returns:** `CalendarWeekView<TItem>`
|
|
190
|
+
|
|
191
|
+
#### `buildDay(date, options?)`
|
|
192
|
+
|
|
193
|
+
Build a day view with time slots.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
- `date` (PlainDate): The target date
|
|
197
|
+
- `options` (object, optional):
|
|
198
|
+
- `startHour` (number): Start hour for time slots
|
|
199
|
+
- `endHour` (number): End hour for time slots
|
|
200
|
+
- `slotDuration` (number): Minutes per slot
|
|
201
|
+
- `today` (PlainDate): Override today's date
|
|
202
|
+
- `data` (TItem[]): Items to include
|
|
203
|
+
- `accessor` (CalendarAccessor<TItem>): Data accessor
|
|
204
|
+
|
|
205
|
+
**Returns:** `CalendarDayView<TItem>`
|
|
206
|
+
|
|
207
|
+
### Formatting Utilities
|
|
208
|
+
|
|
209
|
+
#### `getWeekdays(weekStartsOn?, locale?, format?)`
|
|
210
|
+
|
|
211
|
+
Get localized weekday names.
|
|
212
|
+
|
|
213
|
+
**Parameters:**
|
|
214
|
+
- `weekStartsOn` (0-6, optional): First day of week (default: 0 = Sunday)
|
|
215
|
+
- `locale` (string, optional): BCP 47 locale (default: system locale)
|
|
216
|
+
- `format` ('long' | 'short' | 'narrow', optional): Name format (default: 'short')
|
|
217
|
+
|
|
218
|
+
**Returns:** `string[]` - Array of 7 weekday names
|
|
219
|
+
|
|
220
|
+
**Example:**
|
|
221
|
+
```typescript
|
|
222
|
+
import { getWeekdays } from '@gobrand/calendar-core';
|
|
223
|
+
|
|
224
|
+
getWeekdays(1); // ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
225
|
+
getWeekdays(0); // ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
226
|
+
getWeekdays(1, 'es-ES'); // ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
|
|
227
|
+
getWeekdays(1, 'en-US', 'long'); // ["Monday", "Tuesday", ...]
|
|
228
|
+
getWeekdays(1, 'en-US', 'narrow'); // ["M", "T", "W", "T", "F", "S", "S"]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `getMonthName(month, locale?)`
|
|
232
|
+
|
|
233
|
+
Get localized month name.
|
|
234
|
+
|
|
235
|
+
**Parameters:**
|
|
236
|
+
- `month` (PlainYearMonth): The month to format
|
|
237
|
+
- `locale` (string, optional): BCP 47 locale
|
|
238
|
+
|
|
239
|
+
**Returns:** `string` - Formatted month name
|
|
240
|
+
|
|
241
|
+
**Example:**
|
|
242
|
+
```typescript
|
|
243
|
+
import { getMonthName } from '@gobrand/calendar-core';
|
|
244
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
245
|
+
|
|
246
|
+
const month = Temporal.PlainYearMonth.from('2025-01');
|
|
247
|
+
getMonthName(month); // "January"
|
|
248
|
+
getMonthName(month, 'es-ES'); // "enero"
|
|
249
|
+
getMonthName(month, 'ja-JP'); // "1月"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### `formatTime(time, locale?)`
|
|
253
|
+
|
|
254
|
+
Format a PlainTime as a localized time string.
|
|
255
|
+
|
|
256
|
+
**Parameters:**
|
|
257
|
+
- `time` (PlainTime): Time to format
|
|
258
|
+
- `locale` (string, optional): BCP 47 locale
|
|
259
|
+
|
|
260
|
+
**Returns:** `string` - Formatted time
|
|
261
|
+
|
|
262
|
+
**Example:**
|
|
263
|
+
```typescript
|
|
264
|
+
import { formatTime } from '@gobrand/calendar-core';
|
|
265
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
266
|
+
|
|
267
|
+
const time = Temporal.PlainTime.from('14:30');
|
|
268
|
+
formatTime(time); // "2:30 PM" (en-US)
|
|
269
|
+
formatTime(time, 'en-GB'); // "14:30"
|
|
270
|
+
formatTime(time, 'es-ES'); // "14:30"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Timezone Utilities
|
|
274
|
+
|
|
275
|
+
#### `getMonthRange(timeZone?, weekStartsOn?)`
|
|
276
|
+
|
|
277
|
+
Get the date range for the current month, week-aligned in a specific timezone.
|
|
278
|
+
|
|
279
|
+
**Parameters:**
|
|
280
|
+
- `timeZone` (string, optional): IANA timezone (default: system timezone)
|
|
281
|
+
- `weekStartsOn` (0-6, optional): First day of week (default: 1)
|
|
282
|
+
|
|
283
|
+
**Returns:** `{ start: Temporal.PlainDate; end: Temporal.PlainDate }` - Start/end dates for the week-aligned month
|
|
284
|
+
|
|
285
|
+
**Example:**
|
|
286
|
+
```typescript
|
|
287
|
+
import { getMonthRange } from '@gobrand/calendar-core';
|
|
288
|
+
|
|
289
|
+
const range = getMonthRange('America/New_York', 1);
|
|
290
|
+
// Returns week-aligned range for current month in New York time
|
|
291
|
+
console.log(range.start, range.end);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `getWeekRange(timeZone?, weekStartsOn?)`
|
|
295
|
+
|
|
296
|
+
Get the date range for the current week in a specific timezone.
|
|
297
|
+
|
|
298
|
+
**Parameters:**
|
|
299
|
+
- `timeZone` (string, optional): IANA timezone (default: system timezone)
|
|
300
|
+
- `weekStartsOn` (0-6, optional): First day of week (default: 1)
|
|
301
|
+
|
|
302
|
+
**Returns:** `{ start: Temporal.PlainDate; end: Temporal.PlainDate }` - Start/end dates for the week
|
|
303
|
+
|
|
304
|
+
#### `getDayRange(timeZone?)`
|
|
305
|
+
|
|
306
|
+
Get the date range for today in a specific timezone.
|
|
307
|
+
|
|
308
|
+
**Parameters:**
|
|
309
|
+
- `timeZone` (string, optional): IANA timezone (default: system timezone)
|
|
310
|
+
|
|
311
|
+
**Returns:** `{ start: Temporal.PlainDate; end: Temporal.PlainDate }` - Start/end dates for today (same date)
|
|
312
|
+
|
|
313
|
+
#### `getCurrentTimeZone()`
|
|
314
|
+
|
|
315
|
+
Get the system's current IANA timezone identifier.
|
|
316
|
+
|
|
317
|
+
**Returns:** `string` - IANA timezone (e.g., "America/New_York")
|
|
318
|
+
|
|
319
|
+
#### `convertToTimezone(zdt, timeZone)`
|
|
320
|
+
|
|
321
|
+
Convert a ZonedDateTime to a different timezone.
|
|
322
|
+
|
|
323
|
+
**Parameters:**
|
|
324
|
+
- `zdt` (ZonedDateTime): Source ZonedDateTime
|
|
325
|
+
- `timeZone` (string): Target IANA timezone
|
|
326
|
+
|
|
327
|
+
**Returns:** `ZonedDateTime` - Same instant in new timezone
|
|
328
|
+
|
|
329
|
+
#### `createZonedDateTime(date, time, timeZone)`
|
|
330
|
+
|
|
331
|
+
Create a ZonedDateTime from a PlainDate and PlainTime.
|
|
332
|
+
|
|
333
|
+
**Parameters:**
|
|
334
|
+
- `date` (PlainDate): The date
|
|
335
|
+
- `time` (PlainTime): The time
|
|
336
|
+
- `timeZone` (string): IANA timezone
|
|
337
|
+
|
|
338
|
+
**Returns:** `ZonedDateTime`
|
|
339
|
+
|
|
340
|
+
## Real World Examples
|
|
341
|
+
|
|
342
|
+
### Example 1: HTML Calendar Generator
|
|
343
|
+
|
|
344
|
+
Using core functions without React for maximum flexibility.
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import {
|
|
348
|
+
buildMonth,
|
|
349
|
+
createCalendarAccessor,
|
|
350
|
+
getWeekdays,
|
|
351
|
+
getMonthName
|
|
352
|
+
} from '@gobrand/calendar-core';
|
|
353
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
354
|
+
|
|
355
|
+
type BlogPost = {
|
|
356
|
+
id: string;
|
|
357
|
+
title: string;
|
|
358
|
+
publishedAt: Temporal.PlainDate;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const posts: BlogPost[] = [
|
|
362
|
+
{ id: '1', title: 'Getting Started with Temporal', publishedAt: Temporal.PlainDate.from('2025-01-15') },
|
|
363
|
+
{ id: '2', title: 'Building Calendars', publishedAt: Temporal.PlainDate.from('2025-01-20') },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
const accessor = createCalendarAccessor<BlogPost>({
|
|
367
|
+
getDate: (post) => post.publishedAt,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Build January 2025 calendar
|
|
371
|
+
const month = buildMonth(2025, 1, {
|
|
372
|
+
weekStartsOn: 1,
|
|
373
|
+
data: posts,
|
|
374
|
+
accessor,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Render to HTML
|
|
378
|
+
const weekdays = getWeekdays(1);
|
|
379
|
+
const monthName = getMonthName(month.month);
|
|
380
|
+
|
|
381
|
+
let html = `<h2>${monthName} ${month.month.year}</h2>`;
|
|
382
|
+
html += '<table><thead><tr>';
|
|
383
|
+
weekdays.forEach(day => {
|
|
384
|
+
html += `<th>${day}</th>`;
|
|
385
|
+
});
|
|
386
|
+
html += '</tr></thead><tbody>';
|
|
387
|
+
|
|
388
|
+
month.weeks.forEach(week => {
|
|
389
|
+
html += '<tr>';
|
|
390
|
+
week.forEach(day => {
|
|
391
|
+
const className = day.isCurrentMonth ? '' : 'other-month';
|
|
392
|
+
html += `<td class="${className}">`;
|
|
393
|
+
html += `<div>${day.date.day}</div>`;
|
|
394
|
+
day.items.forEach(post => {
|
|
395
|
+
html += `<div class="post">${post.title}</div>`;
|
|
396
|
+
});
|
|
397
|
+
html += '</td>';
|
|
398
|
+
});
|
|
399
|
+
html += '</tr>';
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
html += '</tbody></table>';
|
|
403
|
+
document.getElementById('calendar')!.innerHTML = html;
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Browser Support
|
|
407
|
+
|
|
408
|
+
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 and Node.js environments.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Development
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# Install dependencies
|
|
418
|
+
pnpm install
|
|
419
|
+
|
|
420
|
+
# Build all packages
|
|
421
|
+
pnpm build
|
|
422
|
+
|
|
423
|
+
# Run tests
|
|
424
|
+
pnpm test --run
|
|
425
|
+
|
|
426
|
+
# Type check
|
|
427
|
+
pnpm typecheck
|
|
428
|
+
|
|
429
|
+
# Release new version
|
|
430
|
+
pnpm release <patch|minor|major>
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Contributing
|
|
434
|
+
|
|
435
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
MIT
|
|
440
|
+
|
|
441
|
+
## Built by Go Brand
|
|
442
|
+
|
|
443
|
+
temporal-calendar is built and maintained by [Go Brand](https://gobrand.app) - a modern social media management platform.
|