@daxence/flexy-date-picker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daxence Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,515 @@
1
+ # flexy-date-picker
2
+
3
+ [![CI](https://github.com/daxence/flexy-date-picker/actions/workflows/ci.yml/badge.svg)](https://github.com/daxence/flexy-date-picker/actions/workflows/ci.yml)
4
+ [![Release](https://github.com/daxence/flexy-date-picker/actions/workflows/release.yml/badge.svg)](https://github.com/daxence/flexy-date-picker/actions/workflows/release.yml)
5
+ [![npm version](https://img.shields.io/npm/v/%40daxence%2Fflexy-date-picker.svg)](https://www.npmjs.com/package/@daxence/flexy-date-picker)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40daxence%2Fflexy-date-picker.svg)](https://www.npmjs.com/package/@daxence/flexy-date-picker)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
8
+ [![React](https://img.shields.io/badge/react-%3E%3D18-61dafb.svg)](https://react.dev)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg)](https://www.typescriptlang.org/)
10
+ [![Test coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](#quality--testing)
11
+ [![Conventional Commits](https://img.shields.io/badge/commits-conventional-fe5196.svg)](https://www.conventionalcommits.org)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
13
+
14
+ A **fully customizable** React date & time picker with single / range selection, an optional time picker, injectable CSS theming, tooltips, and a complete event API. Built with TypeScript, Day.js, and zero hard-coded visual opinions — every color, radius, and slot can be overridden.
15
+
16
+ ![Desktop calendar](./screenshots/desktop-calendar.png)
17
+
18
+
19
+ ## Table of contents
20
+
21
+ - [Features](#features)
22
+ - [Installation](#installation)
23
+ - [Quick start](#quick-start)
24
+ - [Live examples](#live-examples)
25
+ - [Components](#components)
26
+ - [`DatePicker`](#datepicker)
27
+ - [`TimePicker`](#timepicker-standalone)
28
+ - [`CalendarLegend`](#calendarlegend)
29
+ - [Theming via CSS custom properties](#theming-via-css-custom-properties)
30
+ - [Per-slot class / style injection](#per-slot-class--style-injection)
31
+ - [Tooltips](#tooltips)
32
+ - [Disabled dates](#disabled-dates)
33
+ - [Date + time picker](#date--time-picker)
34
+ - [Per-date time schedules](#per-date-time-schedules)
35
+ - [Range picker](#range-picker)
36
+ - [Custom renderers](#custom-renderers)
37
+ - [Accessibility](#accessibility)
38
+ - [Quality & testing](#quality--testing)
39
+ - [Development](#development)
40
+ - [Contributing](#contributing)
41
+ - [License](#license)
42
+
43
+ ---
44
+
45
+ ## Features
46
+
47
+ - ⚛️ React 19 + TypeScript, works with any React ≥ 18 host app
48
+ - 📅 [Day.js](https://day.js.org/) under the hood — no moment, no deprecated libs
49
+ - 🔀 Single-date and date-range selection modes
50
+ - ⏰ Optional built-in time picker (uniform step, custom ranges, or full per-date schedules) and a fully standalone `TimePicker` component
51
+ - 🎨 CSS custom properties — inject any color/shape/spacing without a build step
52
+ - 🧩 Per-slot `classNames` and `styles` props for surgical overrides (30+ slots)
53
+ - 🌍 Locale & RTL-friendly via dayjs locales (`fr`, `ar`, …) and configurable week start
54
+ - 📣 Full event API: `onBeforeSelect`, `onAfterSelect`, `onDateChange`, `onRangeChange`, `onTimeChange`, `onMonthChange`, `onYearChange`, `onDayHover`, `onOpen`, `onClose`, `onClear`
55
+ - 💬 Tooltip support (string, `ReactNode`, or render-prop with rich per-day context)
56
+ - 🖌️ Custom renderers for days, nav buttons, and the input trigger
57
+ - 🧱 `inline` mode to embed the calendar directly in a layout (no popover)
58
+ - ♿ Accessible by default: `role="grid"`, `aria-pressed`, `aria-disabled`, `aria-label`, keyboard-navigable listboxes
59
+ - 🚀 Vite library mode — ESM + UMD builds, tree-shakeable, fully typed
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ npm install @daxence/flexy-date-picker dayjs
67
+ ```
68
+
69
+ ```bash
70
+ pnpm add @daxence/flexy-date-picker dayjs
71
+ ```
72
+
73
+ `dayjs` is a peer dependency and must be installed alongside the package. `react` and `react-dom` (≥ 18) are also required as peer dependencies.
74
+
75
+ ## Quick start
76
+
77
+ ```tsx
78
+ import { DatePicker } from '@daxence/flexy-date-picker';
79
+ import '@daxence/flexy-date-picker/styles'; // import the CSS once, anywhere in your app
80
+
81
+ function App() {
82
+ return <DatePicker mode="single" onDateChange={(date) => console.log(date)} />;
83
+ }
84
+ ```
85
+
86
+ ## Live examples
87
+
88
+ - **Demo app** — run `npm run dev` for a playground covering every prop combination (booking calendars, day content injection, multilingual pickers, time schedules, custom themes, and more). Each demo section includes a "View code" toggle with the exact snippet used.
89
+ - **Storybook** — run `npm run storybook` for isolated, documented stories per component (`DatePicker`, `TimePicker`, `CalendarLegend`), including controls for every prop.
90
+
91
+ ---
92
+
93
+ ## Components
94
+
95
+ ### `DatePicker`
96
+
97
+ The main component. Supports single-date and range selection, an optional time picker, and full styling/theming control.
98
+
99
+ #### Selection & value props
100
+
101
+ | Prop | Type | Default | Description |
102
+ |---|---|---|---|
103
+ | `mode` | `'single' \| 'range'` | `'single'` | Selection mode |
104
+ | `value` | `Dayjs \| null` | — | Controlled single value |
105
+ | `defaultValue` | `Dayjs \| null` | — | Uncontrolled default |
106
+ | `rangeValue` | `{ start, end }` | — | Controlled range value |
107
+ | `defaultRangeValue` | `{ start, end }` | — | Uncontrolled range default |
108
+
109
+ #### Formatting & locale
110
+
111
+ | Prop | Type | Default | Description |
112
+ |---|---|---|---|
113
+ | `format` | `string` | `'MMM D, YYYY'` | dayjs format string |
114
+ | `locale` | `string` | — | dayjs locale (e.g. `'fr'`, `'ar'`) |
115
+ | `lang` | `string` | — | Alias for `locale` |
116
+ | `weekStartsOn` | `0–6` | `0` (Sunday) | First day of week |
117
+
118
+ #### Calendar display
119
+
120
+ | Prop | Type | Default | Description |
121
+ |---|---|---|---|
122
+ | `numberOfMonths` | `number` | `1` | Months displayed at once |
123
+ | `showMonthYearSelectors` | `boolean` | `true` | Show month/year dropdown selectors |
124
+ | `showAdjacentMonthDays` | `boolean` | `false` | Show adjacent-month days in the grid |
125
+ | `showOutsideDays` | `boolean` | — | Deprecated alias for `showAdjacentMonthDays` |
126
+ | `highlightToday` | `boolean` | `true` | Highlight today |
127
+ | `inline` | `boolean` | `false` | Render the calendar directly, without an input/popover |
128
+ | `placement` | `'top' \| 'bottom' \| 'auto'` | `'auto'` | Popover placement |
129
+
130
+ #### Constraints & state
131
+
132
+ | Prop | Type | Default | Description |
133
+ |---|---|---|---|
134
+ | `minDate` / `maxDate` | `Dayjs` | — | Earliest / latest selectable date |
135
+ | `disabled` | `DisabledConfig \| boolean` | — | Disable dates (`before`, `after`, `dates`, `predicate`) |
136
+ | `isDisabled` | `boolean` | `false` | Disable the whole picker |
137
+ | `readOnly` | `boolean` | `false` | Read-only mode |
138
+ | `placeholder` | `string \| [string, string]` | — | Input placeholder(s) |
139
+ | `clearable` | `boolean` | `true` | Show the clear button |
140
+
141
+ #### Time picker (built into `DatePicker`)
142
+
143
+ | Prop | Type | Default | Description |
144
+ |---|---|---|---|
145
+ | `enableTime` | `boolean` | `false` | Show a time slot list alongside the calendar |
146
+ | `timeStep` | `number` | `15` | Uniform slot interval in minutes |
147
+ | `timeFormat` | `string` | `'HH:mm'` | dayjs format for time labels |
148
+ | `minTime` / `maxTime` | `string` (`HH:mm`) | `'00:00'` / `'23:59'` | Bounds for generated slots |
149
+ | `disabledTime` | `DisabledTimeConfig` | — | Disable slots by weekday, date, or predicate |
150
+ | `timeSchedule` | `Record<'YYYY-MM-DD', DateSchedule>` | — | Per-date schedule override (multiple ranges, custom steps, unavailability) |
151
+ | `defaultTimeSchedule` | `DateSchedule` | — | Fallback schedule for dates not present in `timeSchedule` |
152
+
153
+ #### Styling
154
+
155
+ | Prop | Type | Description |
156
+ |---|---|---|
157
+ | `theme` | `DatePickerTheme` | CSS variable overrides (see [theming](#theming-via-css-custom-properties)) |
158
+ | `classNames` | `DatePickerClassNames` | Per-slot className overrides (see [slots](#per-slot-class--style-injection)) |
159
+ | `styles` | `DatePickerStyles` | Per-slot inline style overrides |
160
+ | `className` / `style` | `string` / `CSSProperties` | Extra class/style on the root element |
161
+
162
+ #### Custom renderers
163
+
164
+ | Prop | Signature | Description |
165
+ |---|---|---|
166
+ | `renderDay` | `(date: Dayjs, props: TooltipRenderProps) => ReactNode` | Fully custom day cell content |
167
+ | `renderNavButton` | `(direction: 'prev' \| 'next', onClick: () => void) => ReactNode` | Custom prev/next month buttons |
168
+ | `renderInput` | `(props: RenderInputProps) => ReactNode` | Fully custom trigger input |
169
+
170
+ #### Tooltip
171
+
172
+ | Prop | Type | Default | Description |
173
+ |---|---|---|---|
174
+ | `tooltip` | `string \| ReactNode \| (props: TooltipRenderProps) => ReactNode` | — | Day tooltip content |
175
+ | `tooltipDelay` | `number` | `300` | Tooltip delay (ms) |
176
+
177
+ #### Events
178
+
179
+ | Prop | Signature | Description |
180
+ |---|---|---|
181
+ | `onDateChange` | `(date: Dayjs \| null) => void` | Fires on single-date selection/clear |
182
+ | `onRangeChange` | `(range: { start, end }) => void` | Fires on range selection/clear |
183
+ | `onTimeChange` | `(date: Dayjs \| null, time: string) => void` | Fires when a time slot is picked |
184
+ | `onBeforeSelect` | `(date: Dayjs, meta: DateSelectionMeta) => boolean \| void` | Return `false` to cancel a selection |
185
+ | `onAfterSelect` | `(date: Dayjs, meta: DateSelectionMeta) => void` | Fires right after a selection is committed |
186
+ | `onMonthChange` | `(month: number, year: number) => void` | Fires when the visible month changes |
187
+ | `onYearChange` | `(year: number) => void` | Fires when the visible year changes |
188
+ | `onDayHover` | `(date: Dayjs \| null) => void` | Fires on day hover (range preview) |
189
+ | `onOpen` / `onClose` | `() => void` | Popover open/close |
190
+ | `onClear` | `() => void` | Fires when the clear button is used |
191
+
192
+ ---
193
+
194
+ ### `TimePicker` (standalone)
195
+
196
+ A self-contained time-of-day picker that can be used independently of `DatePicker` — as a popover-triggering input or rendered inline.
197
+
198
+ ```tsx
199
+ import { TimePicker } from '@daxence/flexy-date-picker';
200
+
201
+ <TimePicker
202
+ value={time}
203
+ onChange={setTime}
204
+ timeStep={15}
205
+ minTime="08:00"
206
+ maxTime="18:00"
207
+ />
208
+ ```
209
+
210
+ | Prop | Type | Default | Description |
211
+ |---|---|---|---|
212
+ | `value` | `string` (`HH:mm`) | — | Controlled value |
213
+ | `defaultValue` | `string` | — | Uncontrolled initial value |
214
+ | `onChange` | `(time: string) => void` | — | Called with the selected `HH:mm`, or `''` after clear |
215
+ | `timeStep` | `number` | `15` | Uniform slot interval in minutes (ignored when `schedule` is set) |
216
+ | `minTime` / `maxTime` | `string` (`HH:mm`) | `'00:00'` / `'23:59'` | Slot bounds (ignored when `schedule` is set) |
217
+ | `schedule` | `DateSchedule` | — | Schedule-based generation: multiple time ranges, each with its own step, with gaps between them producing no slots. Overrides `timeStep`/`minTime`/`maxTime` |
218
+ | `timeFormat` | `string` | `'HH:mm'` | dayjs format for slot labels and display |
219
+ | `placeholder` | `string` | — | Placeholder text when no value is selected |
220
+ | `locale` | `string` | — | dayjs locale |
221
+ | `date` | `Dayjs` | today | Reference date used by `disabledTime` |
222
+ | `disabledTime` | `DisabledTimeConfig` | — | Disable slots by weekday, date, or predicate |
223
+ | `inline` | `boolean` | `false` | Render the slot list directly, without an input/popover |
224
+ | `isDisabled` | `boolean` | `false` | Disable all interaction |
225
+ | `readOnly` | `boolean` | `false` | Show the value but prevent changes |
226
+ | `clearable` | `boolean` | `true` | Show the clear button |
227
+ | `onOpen` / `onClose` / `onClear` | `() => void` | — | Popover lifecycle events |
228
+ | `className` / `style` | `string` / `CSSProperties` | — | Root element overrides |
229
+
230
+ ---
231
+
232
+ ### `CalendarLegend`
233
+
234
+ A small presentational component for labeling calendar states (e.g. availability legends in booking UIs).
235
+
236
+ ```tsx
237
+ import { CalendarLegend } from '@daxence/flexy-date-picker';
238
+
239
+ <CalendarLegend
240
+ items={[
241
+ { label: 'Available', className: 'fldp-legend-box--available' },
242
+ { label: 'Selected', className: 'fldp-legend-box--selected' },
243
+ { label: 'Unavailable', className: 'fldp-legend-box--unavailable' },
244
+ ]}
245
+ />
246
+ ```
247
+
248
+ | Prop | Type | Default | Description |
249
+ |---|---|---|---|
250
+ | `items` | `{ label: string \| ReactNode; className: string }[]` | 5 default French labels | Legend rows, each rendered as a colored box + label |
251
+ | `className` | `string` | — | Extra class on the root element |
252
+ | `style` | `CSSProperties` | — | Extra inline style on the root element |
253
+
254
+ ---
255
+
256
+ ## Theming via CSS custom properties
257
+
258
+ Override any token on a parent element:
259
+
260
+ ```css
261
+ .my-app {
262
+ --fldp-primary: #7c3aed;
263
+ --fldp-primary-hover: #6d28d9;
264
+ --fldp-muted: #ede9fe;
265
+ --fldp-border-radius: 12px;
266
+ --fldp-day-border-radius: 50%;
267
+ --fldp-font-family: 'Inter', sans-serif;
268
+ }
269
+ ```
270
+
271
+ Or pass the `theme` prop:
272
+
273
+ ```tsx
274
+ <DatePicker
275
+ lang="fr"
276
+ weekStartsOn={1}
277
+ theme={{
278
+ primary: '#7c3aed',
279
+ primaryHover: '#6d28d9',
280
+ muted: '#ede9fe',
281
+ borderRadius: '12px',
282
+ dayBorderRadius: '50%',
283
+ }}
284
+ />
285
+ ```
286
+
287
+ ### All available tokens
288
+
289
+ | Token | Default | Description |
290
+ |---|---|---|
291
+ | `--fldp-primary` | `#3b82f6` | Accent / selected color |
292
+ | `--fldp-primary-hover` | `#2563eb` | Accent hover |
293
+ | `--fldp-primary-foreground` | `#ffffff` | Text on accent background |
294
+ | `--fldp-background` | `#ffffff` | Popover background |
295
+ | `--fldp-surface` | `rgba(255, 255, 255, 0.88)` | Trigger + menu surface |
296
+ | `--fldp-surface-strong` | `rgba(255, 255, 255, 0.98)` | Strong surface tone |
297
+ | `--fldp-foreground` | `#0f172a` | Default text color |
298
+ | `--fldp-muted` | `#eff6ff` | In-range highlight |
299
+ | `--fldp-accent-soft` | `rgba(15, 118, 110, 0.12)` | Soft hover/accent state |
300
+ | `--fldp-muted-foreground` | `#64748b` | Placeholder / weekday text |
301
+ | `--fldp-border` | `#e2e8f0` | Border color |
302
+ | `--fldp-input-background` | `#ffffff` | Input background |
303
+ | `--fldp-today-color` | `#3b82f6` | Today dot + text |
304
+ | `--fldp-border-radius` | `8px` | Outer radius |
305
+ | `--fldp-day-border-radius` | `6px` | Day cell radius |
306
+ | `--fldp-shadow` | `…` | Popover box shadow |
307
+ | `--fldp-font-family` | system-ui | Font family |
308
+ | `--fldp-font-size` | `14px` | Base font size |
309
+ | `--fldp-disabled-opacity` | `0.38` | Disabled element opacity |
310
+ | `--fldp-transition-duration` | `150ms` | Animation speed |
311
+ | `--fldp-z-index` | `9999` | Popover z-index |
312
+
313
+ ---
314
+
315
+ ## Per-slot class / style injection
316
+
317
+ Every visual slot accepts a className and inline style:
318
+
319
+ ```tsx
320
+ <DatePicker
321
+ classNames={{
322
+ calendar: 'my-calendar',
323
+ daySelected: 'my-selected-day',
324
+ popover: 'my-popover',
325
+ }}
326
+ styles={{
327
+ popover: { background: '#fefce8' },
328
+ dayInRange: { fontWeight: 700 },
329
+ }}
330
+ />
331
+ ```
332
+
333
+ Available slots: `root`, `inputWrapper`, `input`, `separator`, `clearButton`, `popover`, `calendar`, `calendarHeader`, `navButton`, `monthYearLabel`, `weekdays`, `weekday`, `daysGrid`, `day`, `dayToday`, `daySelected`, `dayRangeStart`, `dayRangeEnd`, `dayInRange`, `dayDisabled`, `dayOutside`, `dayHovered`, `timePicker`, `timeTitle`, `timeSlotList`, `timeSlot`, `timeSlotSelected`, `timeSlotDisabled`, `tooltip`.
334
+
335
+ ---
336
+
337
+ ## Tooltips
338
+
339
+ ```tsx
340
+ // Static string
341
+ <DatePicker tooltip="Click to select" />
342
+
343
+ // ReactNode
344
+ <DatePicker tooltip={<strong>Pick a date</strong>} />
345
+
346
+ // Render prop — full control, including hotel-style "X nights" hints
347
+ <DatePicker
348
+ mode="range"
349
+ tooltip={({ date, isToday, isDisabled, isRangeStart, isRangeEnd, isInRange, nights }) => {
350
+ if (isDisabled) return '🚫 Unavailable';
351
+ if (nights && isRangeEnd) return `${nights} night${nights > 1 ? 's' : ''}`;
352
+ if (isRangeStart) return '📌 Check-in';
353
+ if (isRangeEnd) return '🏁 Check-out';
354
+ if (isToday) return '📅 Today';
355
+ return date.format('dddd, MMM D');
356
+ }}
357
+ tooltipDelay={200}
358
+ />
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Disabled dates
364
+
365
+ ```tsx
366
+ <DatePicker
367
+ disabled={{
368
+ before: dayjs('2024-01-01'), // disable all before Jan 1
369
+ after: dayjs('2024-12-31'), // disable all after Dec 31
370
+ dates: [dayjs('2024-06-15')], // specific dates
371
+ predicate: (d) => d.day() === 0 || d.day() === 6, // weekends
372
+ }}
373
+ />
374
+ ```
375
+
376
+ ---
377
+
378
+ ## Date + time picker
379
+
380
+ Combine calendar selection with a time slot list in one component:
381
+
382
+ ```tsx
383
+ <DatePicker
384
+ mode="single"
385
+ value={meetingDate}
386
+ onDateChange={setMeetingDate}
387
+ onTimeChange={(date, time) => console.log(date, time)}
388
+ enableTime
389
+ timeStep={15}
390
+ timeFormat="HH:mm"
391
+ minTime="08:00"
392
+ maxTime="20:00"
393
+ disabledTime={{
394
+ weekdays: { 0: ['08:00', '08:15', '08:30'], 6: ['08:00', '08:15', '08:30'] },
395
+ }}
396
+ />
397
+ ```
398
+
399
+ ## Per-date time schedules
400
+
401
+ Give individual dates their own set of time ranges — with gaps, different step intervals, or full unavailability — via `timeSchedule` / `defaultTimeSchedule`:
402
+
403
+ ```tsx
404
+ <DatePicker
405
+ mode="single"
406
+ enableTime
407
+ defaultTimeSchedule={{ ranges: [{ from: '09:00', to: '17:00', step: 30 }] }}
408
+ timeSchedule={{
409
+ '2026-06-15': { unavailable: true }, // fully booked, no slots shown
410
+ '2026-06-16': {
411
+ ranges: [
412
+ { from: '08:00', to: '12:00', step: 15 },
413
+ { from: '14:00', to: '16:00', step: 5 },
414
+ { from: '16:00', to: '18:00', step: 10 },
415
+ ],
416
+ },
417
+ }}
418
+ />
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Range picker
424
+
425
+ ```tsx
426
+ const [range, setRange] = useState({ start: null, end: null });
427
+
428
+ <DatePicker
429
+ mode="range"
430
+ rangeValue={range}
431
+ onRangeChange={setRange}
432
+ numberOfMonths={2}
433
+ />
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Custom renderers
439
+
440
+ ```tsx
441
+ <DatePicker
442
+ renderDay={(date, { isSelected }) => (
443
+ <span style={{ fontWeight: isSelected ? 700 : 400 }}>{date.date()}</span>
444
+ )}
445
+ renderNavButton={(direction, onClick) => (
446
+ <button onClick={onClick}>{direction === 'prev' ? '←' : '→'}</button>
447
+ )}
448
+ renderInput={({ value, onClick, isOpen }) => (
449
+ <button onClick={onClick}>{value || 'Pick a date'} {isOpen ? '▲' : '▼'}</button>
450
+ )}
451
+ />
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Accessibility
457
+
458
+ The calendar grid, navigation, and inputs follow WAI-ARIA authoring practices out of the box:
459
+
460
+ - Day grid uses `role="grid"` / `role="row"` / `role="columnheader"`, with each day exposing `aria-label` (full localized date), `aria-pressed` (selected state), and `aria-disabled`.
461
+ - Month/year navigation exposes `aria-label="Previous month"` / `"Next month"`, and the month/year dropdowns use `role="listbox"` / `role="option"` with `aria-expanded` / `aria-selected`.
462
+ - The input trigger uses `role="button"`, `aria-haspopup="dialog"`, and `aria-expanded`; the popover itself uses `role="dialog"`.
463
+ - The standalone time picker region is exposed via `role="region"` with a descriptive `aria-label`.
464
+ - Decorative icons/carets are marked `aria-hidden="true"` so they're skipped by screen readers.
465
+
466
+ ---
467
+
468
+ ## Quality & testing
469
+
470
+ This project is built and gated the way a production library should be:
471
+
472
+ | Check | Status |
473
+ |---|---|
474
+ | **Unit test coverage** | 100% statements / branches / functions / lines, enforced as a hard CI gate (`vitest --coverage`, thresholds in [vite.config.ts](./vite.config.ts)) |
475
+ | **Test suite** | 100+ tests across every component, context, and utility (`vitest`, React Testing Library) |
476
+ | **Type safety** | `strict` TypeScript, zero `any` leaking through the public API, full `.d.ts` output |
477
+ | **Linting** | ESLint with `typescript-eslint`, `react-hooks`, `jsx-a11y`, and `unicorn` rule sets, zero warnings allowed |
478
+ | **Accessibility** | WAI-ARIA roles/attributes on every interactive element (see [Accessibility](#accessibility)) and an automated `@storybook/addon-a11y` check on every story |
479
+ | **Bundle** | Tree-shakeable ESM + UMD output via `vite-plugin-dts`, ~35 kB gzipped for the full library |
480
+ | **Commits & releases** | [Conventional Commits](https://www.conventionalcommits.org/) enforced via `commitlint` + `husky`, versioning and changelog automated with `semantic-release` |
481
+
482
+ Every prop, event, and edge case documented above (disabled dates, per-date time schedules, range reordering, keyboard navigation, placement auto-resolution, etc.) is backed by a dedicated test — the goal is that the README and the test suite never drift apart.
483
+
484
+ ```bash
485
+ npm run test # Run the full suite
486
+ npm run test:coverage # Run with the 100% coverage gate enforced
487
+ npm run lint # Zero-warning lint pass
488
+ ```
489
+
490
+ ---
491
+
492
+ ## Development
493
+
494
+ ```bash
495
+ npm install
496
+ npm run dev # Vite dev server with the demo app (with copy-pasteable code snippets)
497
+ npm run build # Build the library to /dist
498
+ npm run test # Run the test suite
499
+ npm run test:coverage # Run tests with coverage
500
+ npm run storybook # Explore components interactively
501
+ npm run build-storybook # Build a static Storybook site
502
+ npm run lint # Lint the codebase
503
+ ```
504
+
505
+ ## Contributing
506
+
507
+ Issues and pull requests are welcome. Please:
508
+
509
+ 1. Keep the codebase feature-based (`src/features/<feature>/...`) and add tests + Storybook stories for new behavior.
510
+ 2. Run `npm run lint` and `npm run test` before opening a PR.
511
+ 3. Follow [Conventional Commits](https://www.conventionalcommits.org/) — releases are automated with `semantic-release`.
512
+
513
+ ## License
514
+
515
+ [MIT](./LICENSE) © [Daxence Solutions](https://github.com/daxence)