@acusti/date-picker 0.11.1 → 0.13.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/README.md CHANGED
@@ -5,70 +5,668 @@
5
5
  [![bundle size](https://deno.bundlejs.com/badge?q=@acusti/date-picker)](https://bundlejs.com/?q=%40acusti%2Fdate-picker)
6
6
  [![supply chain security](https://socket.dev/api/badge/npm/package/@acusti/date-picker/0.8.0)](https://socket.dev/npm/package/@acusti/date-picker/overview/0.8.0)
7
7
 
8
- A group of React components and utils for rendering a date picker with
9
- support for ranges via a two-up month calendar view.
8
+ A comprehensive React date picker library with support for single date
9
+ selection, date ranges, and two-up month calendar views. Built with
10
+ accessibility and user experience in mind, featuring smooth navigation,
11
+ intelligent date range handling, and customizable styling.
10
12
 
11
- See the [storybook docs and demo][] to get a feel for what it can do.
13
+ ## Key Features
12
14
 
13
- [storybook docs and demo]:
14
- https://uikit.acusti.ca/?path=/docs/uikit-controls-datepicker-datepicker--docs
15
+ - **Single & Range Selection** - Pick individual dates or date ranges with
16
+ intelligent range logic
17
+ - **Two-Up Calendar View** - Display two months side-by-side for easier
18
+ range selection
19
+ - **Month Navigation** - Smooth navigation with optional month limits for
20
+ business logic
21
+ - **Smart Range Handling** - Automatic date swapping and preview
22
+ highlighting
23
+ - **Flexible Date Input** - Accepts Date objects, ISO strings, or
24
+ timestamps
25
+ - **Customizable Display** - Month abbreviations, custom styling, and
26
+ layout options
27
+ - **Built-in Styling** - Attractive default styles with CSS custom property
28
+ theming
29
+ - **Accessibility Ready** - Keyboard navigation and screen reader support
15
30
 
16
- ## Usage
31
+ ## Installation
17
32
 
18
- ```
33
+ ```bash
19
34
  npm install @acusti/date-picker
20
35
  # or
21
36
  yarn add @acusti/date-picker
22
37
  ```
23
38
 
24
- ### Example
25
-
26
- To render a two-up date picker for selecting date ranges, handling date
27
- selections via the `onChange` prop and showing months using abbreviations:
39
+ ## Quick Start
28
40
 
29
41
  ```tsx
30
42
  import { DatePicker } from '@acusti/date-picker';
31
43
  import { useState } from 'react';
32
44
 
33
- function Popover() {
34
- const [dateRangeStart, setDateRangeStart] = useState<null | string>(
35
- null,
45
+ // Simple single date picker
46
+ function SimpleDatePicker() {
47
+ const [selectedDate, setSelectedDate] = useState('');
48
+
49
+ return (
50
+ <DatePicker
51
+ onChange={({ dateStart }) => setSelectedDate(dateStart)}
52
+ dateStart={selectedDate}
53
+ />
36
54
  );
37
- const [dateRangeEnd, setDateRangeEnd] = useState<null | string>(null);
55
+ }
38
56
 
39
- const handleDateRangeChange = ({ dateEnd, dateStart }) => {
40
- setDateRangeStart(dateStart);
41
- if (dateEnd) {
42
- setDateRangeEnd(dateEnd);
43
- }
57
+ // Date range picker
58
+ function DateRangePicker() {
59
+ const [dateRange, setDateRange] = useState({ start: '', end: '' });
60
+
61
+ const handleChange = ({ dateStart, dateEnd }) => {
62
+ setDateRange({ start: dateStart, end: dateEnd || '' });
44
63
  };
45
64
 
46
65
  return (
47
66
  <DatePicker
48
67
  isRange
49
68
  isTwoUp
50
- onChange={handleDateRangeChange}
69
+ onChange={handleChange}
70
+ dateStart={dateRange.start}
71
+ dateEnd={dateRange.end}
51
72
  useMonthAbbreviations
52
73
  />
53
74
  );
54
75
  }
55
76
  ```
56
77
 
57
- ### Props
78
+ ## API Reference
58
79
 
59
- This is the type signature for the props you can pass to `DatePicker`:
80
+ ### DatePicker Component
60
81
 
61
- ```ts
82
+ ```tsx
62
83
  type Props = {
84
+ /** Additional CSS class name for styling */
63
85
  className?: string;
64
- dateEnd?: Date | string | number;
65
- dateStart?: Date | string | number;
86
+
87
+ /** End date for range selection (Date object, ISO string, timestamp, or null) */
88
+ dateEnd?: Date | string | number | null;
89
+
90
+ /** Start date for single or range selection (Date object, ISO string, timestamp, or null) */
91
+ dateStart?: Date | string | number | null;
92
+
93
+ /** Initial month to display (number of months since January 1970) */
66
94
  initialMonth?: number;
95
+
96
+ /** Enable date range selection mode */
67
97
  isRange?: boolean;
98
+
99
+ /** Display two months side-by-side */
68
100
  isTwoUp?: boolean;
101
+
102
+ /** Earliest month that can be navigated to */
69
103
  monthLimitFirst?: number;
104
+
105
+ /** Latest month that can be navigated to */
70
106
  monthLimitLast?: number;
71
- onChange: (payload: { dateEnd?: string; dateStart: string }) => void;
107
+
108
+ /** Callback when dates are selected */
109
+ onChange: (payload: {
110
+ dateEnd?: string | null;
111
+ dateStart: string;
112
+ }) => void;
113
+
114
+ /** Show end date’s month initially (when both start and end dates exist) */
115
+ showEndInitially?: boolean;
116
+
117
+ /** Use abbreviated month names (Jan, Feb, etc.) */
72
118
  useMonthAbbreviations?: boolean;
73
119
  };
74
120
  ```
121
+
122
+ ### MonthCalendar Component
123
+
124
+ For advanced use cases, you can use the individual month calendar:
125
+
126
+ ```tsx
127
+ import { MonthCalendar } from '@acusti/date-picker';
128
+
129
+ type MonthCalendarProps = {
130
+ className?: string;
131
+ dateEnd?: Date | string | number | null;
132
+ dateEndPreview?: string | null;
133
+ dateStart?: Date | string | number | null;
134
+ isRange?: boolean;
135
+ month: number; // Months since January 1970
136
+ onChange?: (date: string) => void;
137
+ onChangeEndPreview?: (date: string) => void;
138
+ title?: string;
139
+ };
140
+ ```
141
+
142
+ ### Utility Functions
143
+
144
+ ```tsx
145
+ import {
146
+ getMonthFromDate,
147
+ getYearFromMonth,
148
+ getMonthNameFromMonth,
149
+ getMonthAbbreviationFromMonth,
150
+ } from '@acusti/date-picker';
151
+
152
+ // Convert Date to month number (months since Jan 1970)
153
+ const monthNumber = getMonthFromDate(new Date());
154
+
155
+ // Convert month number to calendar year
156
+ const year = getYearFromMonth(monthNumber);
157
+
158
+ // Get full month name
159
+ const monthName = getMonthNameFromMonth(monthNumber); // "January"
160
+
161
+ // Get abbreviated month name
162
+ const monthAbbr = getMonthAbbreviationFromMonth(monthNumber); // "Jan"
163
+ ```
164
+
165
+ ## Usage Examples
166
+
167
+ ### Booking System Date Range
168
+
169
+ **[🎮 Live Demo](https://uikit.acusti.ca/?path=/story/uikit-controls-datepicker-datepicker--booking-system-date-range)**
170
+
171
+ ```tsx
172
+ import { DatePicker } from '@acusti/date-picker';
173
+ import { useState } from 'react';
174
+
175
+ function BookingDatePicker() {
176
+ const [checkIn, setCheckIn] = useState('');
177
+ const [checkOut, setCheckOut] = useState('');
178
+ const isValid = checkIn && checkOut;
179
+
180
+ // Limit to next 12 months only
181
+ const today = new Date();
182
+ const monthLimitFirst = getMonthFromDate(today);
183
+ const monthLimitLast = monthLimitFirst + 12;
184
+
185
+ return (
186
+ <div className="booking-date-picker">
187
+ <h3>Select Your Stay</h3>
188
+ <DatePicker
189
+ className="booking-date-picker-story"
190
+ isRange
191
+ isTwoUp
192
+ monthLimitFirst={monthLimitFirst}
193
+ monthLimitLast={monthLimitLast}
194
+ onChange={({ dateStart, dateEnd }) => {
195
+ setCheckIn(dateStart);
196
+ setCheckOut(dateEnd ?? '');
197
+ }}
198
+ dateStart={checkIn}
199
+ dateEnd={checkOut}
200
+ useMonthAbbreviations
201
+ />
202
+
203
+ {isValid ? (
204
+ <div
205
+ className="booking-summary"
206
+ style={{
207
+ marginTop: '16px',
208
+ padding: '16px',
209
+ border: '1px solid #e1e5e9',
210
+ borderRadius: '8px',
211
+ backgroundColor: '#f8f9fa',
212
+ }}
213
+ >
214
+ <p>
215
+ <strong>Check-in:</strong>{' '}
216
+ {new Date(checkIn).toLocaleDateString()}
217
+ </p>
218
+ <p>
219
+ <strong>Check-out:</strong>{' '}
220
+ {new Date(checkOut).toLocaleDateString()}
221
+ </p>
222
+ <p>
223
+ <strong>Duration:</strong>{' '}
224
+ {(() => {
225
+ const checkInTime = new Date(
226
+ checkIn,
227
+ ).getTime();
228
+ const checkOutTime = new Date(
229
+ checkOut,
230
+ ).getTime();
231
+ const diffTime = checkOutTime - checkInTime;
232
+ const diffDays = Math.ceil(
233
+ diffTime / (1000 * 60 * 60 * 24),
234
+ );
235
+ return Math.max(0, diffDays);
236
+ })()}{' '}
237
+ nights
238
+ </p>
239
+ </div>
240
+ ) : null}
241
+ </div>
242
+ );
243
+ }
244
+ ```
245
+
246
+ ### Event Scheduler
247
+
248
+ **[🎮 Live Demo](https://uikit.acusti.ca/?path=/story/uikit-controls-datepicker-datepicker--event-scheduler)**
249
+
250
+ ```tsx
251
+ import { DatePicker } from '@acusti/date-picker';
252
+ import { useState } from 'react';
253
+
254
+ function EventScheduler() {
255
+ const [eventDate, setEventDate] = useState('');
256
+ const [showPicker, setShowPicker] = useState(false);
257
+
258
+ // Only allow future dates
259
+ const monthLimitFirst = getMonthFromDate(new Date());
260
+
261
+ return (
262
+ <div className="event-scheduler">
263
+ <div style={{ marginBottom: '16px' }}>
264
+ <label
265
+ htmlFor="event-date"
266
+ style={{
267
+ display: 'block',
268
+ marginBottom: '8px',
269
+ fontWeight: 500,
270
+ }}
271
+ >
272
+ Event Date:
273
+ </label>
274
+ <input
275
+ id="event-date"
276
+ type="text"
277
+ value={
278
+ eventDate
279
+ ? new Date(eventDate).toLocaleDateString()
280
+ : ''
281
+ }
282
+ onClick={() => setShowPicker(true)}
283
+ placeholder="Click to select date"
284
+ readOnly
285
+ style={{
286
+ padding: '8px 12px',
287
+ border: '2px solid #e1e5e9',
288
+ borderRadius: '6px',
289
+ cursor: 'pointer',
290
+ width: '200px',
291
+ }}
292
+ />
293
+ </div>
294
+
295
+ {showPicker ? (
296
+ <div
297
+ style={{
298
+ position: 'relative',
299
+ zIndex: 1000,
300
+ marginTop: '8px',
301
+ }}
302
+ >
303
+ <div
304
+ style={{
305
+ padding: '16px',
306
+ border: '1px solid #e1e5e9',
307
+ borderRadius: '8px',
308
+ backgroundColor: 'white',
309
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
310
+ }}
311
+ >
312
+ <DatePicker
313
+ className="event-scheduler-story"
314
+ monthLimitFirst={monthLimitFirst}
315
+ onChange={({ dateStart }) => {
316
+ setEventDate(dateStart);
317
+ setShowPicker(false);
318
+ }}
319
+ dateStart={eventDate}
320
+ />
321
+ <button
322
+ onClick={() => setShowPicker(false)}
323
+ style={{
324
+ marginTop: '12px',
325
+ padding: '8px 16px',
326
+ border: '1px solid #ccc',
327
+ borderRadius: '4px',
328
+ cursor: 'pointer',
329
+ }}
330
+ >
331
+ Cancel
332
+ </button>
333
+ </div>
334
+ </div>
335
+ ) : null}
336
+ </div>
337
+ );
338
+ }
339
+ ```
340
+
341
+ ### Report Date Range Filter
342
+
343
+ ```tsx
344
+ import { DatePicker } from '@acusti/date-picker';
345
+ import { useState, useEffect } from 'react';
346
+
347
+ function ReportFilter() {
348
+ const [dateRange, setDateRange] = useState({
349
+ start: '',
350
+ end: '',
351
+ });
352
+ const [isOpen, setIsOpen] = useState(false);
353
+
354
+ // Limit to past 2 years for historical reports
355
+ const today = new Date();
356
+ const monthLimitLast = getMonthFromDate(today);
357
+ const monthLimitFirst = monthLimitLast - 24;
358
+
359
+ const handleApplyRange = ({ dateStart, dateEnd }) => {
360
+ setDateRange({
361
+ start: dateStart,
362
+ end: dateEnd || dateStart,
363
+ });
364
+
365
+ if (dateEnd) {
366
+ setIsOpen(false);
367
+ }
368
+ };
369
+
370
+ const formatDateRange = () => {
371
+ if (!dateRange.start) return 'Select date range';
372
+
373
+ const startDate = new Date(dateRange.start).toLocaleDateString();
374
+ const endDate = dateRange.end
375
+ ? new Date(dateRange.end).toLocaleDateString()
376
+ : startDate;
377
+
378
+ return `${startDate} - ${endDate}`;
379
+ };
380
+
381
+ return (
382
+ <div className="report-filter">
383
+ <button
384
+ className="date-range-button"
385
+ onClick={() => setIsOpen(!isOpen)}
386
+ >
387
+ 📅 {formatDateRange()}
388
+ </button>
389
+
390
+ {isOpen ? (
391
+ <div className="date-picker-dropdown">
392
+ <DatePicker
393
+ isRange
394
+ isTwoUp
395
+ monthLimitFirst={monthLimitFirst}
396
+ monthLimitLast={monthLimitLast}
397
+ onChange={handleApplyRange}
398
+ dateStart={dateRange.start}
399
+ dateEnd={dateRange.end}
400
+ showEndInitially={!!dateRange.end}
401
+ />
402
+ </div>
403
+ ) : null}
404
+ </div>
405
+ );
406
+ }
407
+ ```
408
+
409
+ ### Birthday Picker with Year Limits
410
+
411
+ **[🎮 Live Demo](https://uikit.acusti.ca/?path=/story/uikit-controls-datepicker-datepicker--birthday-picker)**
412
+
413
+ ```tsx
414
+ import { DatePicker, getMonthFromDate } from '@acusti/date-picker';
415
+ import { useState } from 'react';
416
+
417
+ function BirthdayPicker() {
418
+ const [birthday, setBirthday] = useState('');
419
+
420
+ // Reasonable age limits: 13 to 120 years ago
421
+ const today = new Date();
422
+ const maxAge = new Date(
423
+ today.getFullYear() - 120,
424
+ today.getMonth(),
425
+ today.getDate(),
426
+ );
427
+ const minAge = new Date(
428
+ today.getFullYear() - 13,
429
+ today.getMonth(),
430
+ today.getDate(),
431
+ );
432
+
433
+ const monthLimitFirst = getMonthFromDate(maxAge);
434
+ const monthLimitLast = getMonthFromDate(minAge);
435
+
436
+ // Start showing calendar at a reasonable age (25 years ago)
437
+ const defaultMonth = getMonthFromDate(
438
+ new Date(
439
+ today.getFullYear() - 25,
440
+ today.getMonth(),
441
+ today.getDate(),
442
+ ),
443
+ );
444
+
445
+ return (
446
+ <div className="birthday-picker">
447
+ <h3>Enter Your Birthday</h3>
448
+ <DatePicker
449
+ initialMonth={defaultMonth}
450
+ monthLimitFirst={monthLimitFirst}
451
+ monthLimitLast={monthLimitLast}
452
+ onChange={({ dateStart }) => setBirthday(dateStart)}
453
+ dateStart={birthday}
454
+ />
455
+
456
+ {birthday ? (
457
+ <p
458
+ style={{
459
+ marginTop: '16px',
460
+ padding: '12px',
461
+ backgroundColor: '#e3f2fd',
462
+ borderRadius: '6px',
463
+ }}
464
+ >
465
+ <strong>
466
+ You are{' '}
467
+ {Math.floor(
468
+ (today.getTime() -
469
+ new Date(birthday).getTime()) /
470
+ (1000 * 60 * 60 * 24 * 365.25),
471
+ )}{' '}
472
+ years old
473
+ </strong>
474
+ </p>
475
+ ) : null}
476
+ </div>
477
+ );
478
+ }
479
+ ```
480
+
481
+ ### Multi-Month Navigation
482
+
483
+ **[🎮 Live Demo](https://uikit.acusti.ca/?path=/story/uikit-controls-datepicker-datepicker--flexible-date-picker)**
484
+
485
+ ```tsx
486
+ import { DatePicker, getMonthFromDate } from '@acusti/date-picker';
487
+ import { useState } from 'react';
488
+
489
+ function FlexibleDatePicker() {
490
+ const [selectedDate, setSelectedDate] = useState('');
491
+ const [viewMode, setViewMode] = useState<'single' | 'double'>(
492
+ 'single',
493
+ );
494
+
495
+ return (
496
+ <div className="flexible-date-picker">
497
+ <div style={{ marginBottom: '16px' }}>
498
+ <label style={{ marginRight: '16px' }}>
499
+ <input
500
+ type="radio"
501
+ checked={viewMode === 'single'}
502
+ onChange={() => setViewMode('single')}
503
+ style={{ marginRight: '4px' }}
504
+ />
505
+ Single Month
506
+ </label>
507
+ <label>
508
+ <input
509
+ type="radio"
510
+ checked={viewMode === 'double'}
511
+ onChange={() => setViewMode('double')}
512
+ style={{ marginRight: '4px' }}
513
+ />
514
+ Two Months
515
+ </label>
516
+ </div>
517
+
518
+ <DatePicker
519
+ isTwoUp={viewMode === 'double'}
520
+ useMonthAbbreviations={viewMode === 'double'}
521
+ onChange={({ dateStart }) => setSelectedDate(dateStart)}
522
+ dateStart={selectedDate}
523
+ />
524
+
525
+ {selectedDate ? (
526
+ <div
527
+ style={{
528
+ marginTop: '16px',
529
+ padding: '12px',
530
+ border: '1px solid #e1e5e9',
531
+ borderRadius: '6px',
532
+ }}
533
+ >
534
+ <strong>Selected:</strong>{' '}
535
+ {new Date(selectedDate).toLocaleDateString()}
536
+ <br />
537
+ <strong>Day of week:</strong>{' '}
538
+ {new Date(selectedDate).toLocaleDateString('en', {
539
+ weekday: 'long',
540
+ })}
541
+ </div>
542
+ ) : null}
543
+ </div>
544
+ );
545
+ }
546
+ ```
547
+
548
+ ### Custom Month Calendar Usage
549
+
550
+ ```tsx
551
+ import { MonthCalendar, getMonthFromDate } from '@acusti/date-picker';
552
+ import { useState } from 'react';
553
+
554
+ function CustomCalendarGrid() {
555
+ const [selectedDates, setSelectedDates] = useState<string[]>([]);
556
+ const currentMonth = getMonthFromDate(new Date());
557
+
558
+ const handleDateSelect = (date: string) => {
559
+ setSelectedDates((prev) =>
560
+ prev.includes(date)
561
+ ? prev.filter((d) => d !== date)
562
+ : [...prev, date],
563
+ );
564
+ };
565
+
566
+ return (
567
+ <div>
568
+ <h3>Multi-Select Calendar</h3>
569
+ <p>Click dates to select/deselect multiple dates</p>
570
+
571
+ <MonthCalendar
572
+ month={currentMonth}
573
+ onChange={handleDateSelect}
574
+ title="Select Multiple Dates"
575
+ />
576
+
577
+ <div className="selected-dates">
578
+ <h4>Selected Dates ({selectedDates.length}):</h4>
579
+ <ul>
580
+ {selectedDates.map((date) => (
581
+ <li key={date}>
582
+ {new Date(date).toLocaleDateString()}
583
+ </li>
584
+ ))}
585
+ </ul>
586
+ </div>
587
+ </div>
588
+ );
589
+ }
590
+ ```
591
+
592
+ ## Styling
593
+
594
+ The date picker uses CSS custom properties for easy theming:
595
+
596
+ ```css
597
+ .date-picker {
598
+ /* Calendar colors */
599
+ --date-picker-bg: #ffffff;
600
+ --date-picker-border: #e0e0e0;
601
+ --date-picker-text: #333333;
602
+
603
+ /* Selected date colors */
604
+ --date-picker-selected-bg: #007bff;
605
+ --date-picker-selected-text: #ffffff;
606
+
607
+ /* Range selection colors */
608
+ --date-picker-range-bg: #e3f2fd;
609
+ --date-picker-range-border: #2196f3;
610
+
611
+ /* Hover states */
612
+ --date-picker-hover-bg: #f5f5f5;
613
+
614
+ /* Navigation arrows */
615
+ --date-picker-arrow-color: #666666;
616
+ --date-picker-arrow-hover: #333333;
617
+
618
+ /* Month header */
619
+ --date-picker-header-text: #333333;
620
+ --date-picker-header-bg: #f8f9fa;
621
+ }
622
+
623
+ /* Custom styling example */
624
+ .booking-calendar {
625
+ --date-picker-selected-bg: #28a745;
626
+ --date-picker-range-bg: #d4edda;
627
+ --date-picker-range-border: #28a745;
628
+ }
629
+
630
+ .event-calendar {
631
+ --date-picker-selected-bg: #6f42c1;
632
+ --date-picker-range-bg: #e2d9f3;
633
+ }
634
+ ```
635
+
636
+ ## Month Number System
637
+
638
+ The date picker uses an internal month numbering system where months are
639
+ represented as the number of months since January 1970:
640
+
641
+ - January 1970 = 0
642
+ - February 1970 = 1
643
+ - January 2024 = 648
644
+ - etc.
645
+
646
+ This system allows for efficient month calculations and navigation. The
647
+ utility functions handle the conversion between this system and standard
648
+ dates.
649
+
650
+ ## Browser Compatibility
651
+
652
+ - **Modern Browsers** - Chrome, Firefox, Safari, Edge (latest versions)
653
+ - **Mobile Support** - iOS Safari, Android Chrome
654
+ - **SSR Compatible** - Works with Next.js, React Router, and other SSR
655
+ frameworks
656
+
657
+ ## Common Use Cases
658
+
659
+ - **Booking Systems** - Hotels, flights, rental properties
660
+ - **Event Management** - Conference registration, appointment scheduling
661
+ - **Reporting Tools** - Date range filters for analytics
662
+ - **Form Inputs** - Birthday selection, deadline setting
663
+ - **Content Management** - Publishing date selection
664
+ - **E-commerce** - Delivery date selection, sale periods
665
+ - **Project Management** - Milestone and deadline tracking
666
+
667
+ ## Demo
668
+
669
+ See the
670
+ [Storybook documentation and examples](https://uikit.acusti.ca/?path=/docs/uikit-controls-datepicker-datepicker--docs)
671
+ for interactive demonstrations of all date picker features and
672
+ configurations.