@mngh/jalali-datepicker 1.1.7 → 1.2.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
@@ -1,630 +1,636 @@
1
- # @mngh/jalali-datepicker
2
-
3
- A modern, headless-friendly Jalali (Persian/Shamsi) Date & Time Picker for React — zero date-library runtime dependencies, fully typed with native `Date` objects, WAI-ARIA accessible, and themeable with Tailwind CSS or CSS variables.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@mngh/jalali-datepicker.svg)](https://www.npmjs.com/package/@mngh/jalali-datepicker)
6
- [![bundlephobia](https://img.shields.io/bundlephobia/minzip/@mngh/jalali-datepicker)](https://bundlephobia.com/package/@mngh/jalali-datepicker)
7
- [![license](https://img.shields.io/npm/l/@mngh/jalali-datepicker.svg)](https://github.com/mngh/jalali-datepicker/blob/main/LICENSE)
8
- [![types](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://github.com/mngh/jalali-datepicker)
9
-
10
- > **Repository:** [github.com/mngh/jalali-datepicker](https://github.com/mngh/jalali-datepicker)
11
- > **Bundle size:** ~38 kB (CJS, unminified) / ~51 kB (ESM, unminified), highly tree-shakeable
12
- > **Peer dependencies:** React 18 or 19
13
-
14
- ---
15
-
16
- ## Table of Contents
17
-
18
- 1. [Introduction & Quick Start](#1-introduction--quick-start)
19
- 2. [Core Concepts & Data Flow](#2-core-concepts--data-flow)
20
- 3. [Display Variants & Selection Modes](#3-display-variants--selection-modes)
21
- 4. [Built-in Plugins & Advanced Features](#4-built-in-plugins--advanced-features)
22
- 5. [Headless Hook Architecture](#5-headless-hook-architecture-usejalalidatepicker)
23
- 6. [Complete API Reference](#6-complete-api-reference)
24
- 7. [Theming & Styling](#7-theming--styling-integration)
25
- 8. [Accessibility & Keyboard Shortcuts](#8-accessibility--keyboard-shortcuts)
26
-
27
- ---
28
-
29
- ## 1. Introduction & Quick Start
30
-
31
- ### Why `@mngh/jalali-datepicker`?
32
-
33
- - **Zero date-library overhead** — no Moment.js, no date-fns, no dayjs. The Jalali/Gregorian conversion math is implemented internally with plain arithmetic.
34
- - **Native `Date` in, native `Date` out** — every prop and callback speaks standard JavaScript `Date` objects. You never touch a custom calendar object.
35
- - **Three display variants** — inline, popover, and modal — and three selection modes — single, range, and multiple.
36
- - **Headless-first** — the entire UI is built on top of a public hook (`useJalaliDatePicker`) that you can use to build your own component from scratch.
37
- - **Accessible by default** — full keyboard navigation, roving tabindex, and ARIA roles (`grid`, `gridcell`, `dialog`) out of the box.
38
- - **Themeable** — Tailwind CSS class slots and CSS custom properties, with a built-in `DatePickerThemeProvider` for light/dark mode.
39
-
40
- ### Installation
41
-
42
- ```bash
43
- npm install @mngh/jalali-datepicker
44
- ```
45
-
46
- ```bash
47
- pnpm add @mngh/jalali-datepicker
48
- ```
49
-
50
- ```bash
51
- yarn add @mngh/jalali-datepicker
52
- ```
53
-
54
- ```bash
55
- bun add @mngh/jalali-datepicker
56
- ```
57
-
58
- ### Quick Start
59
-
60
- ```tsx
61
- import { useState } from "react";
62
- import {
63
- JalaliDatePicker,
64
- DatePickerThemeProvider,
65
- } from "@mngh/jalali-datepicker";
66
-
67
- export default function App() {
68
- const [date, setDate] = useState<Date | null>(null);
69
-
70
- return (
71
- <DatePickerThemeProvider mode="light">
72
- <JalaliDatePicker
73
- variant="popover"
74
- mode="single"
75
- value={date}
76
- onChange={setDate}
77
- placeholder="Select a date"
78
- />
79
- </DatePickerThemeProvider>
80
- );
81
- }
82
- ```
83
-
84
- That's it — `date` is always a plain JavaScript `Date` object (or `null`). No conversion helpers, no adapters.
85
- The package ships with self-contained component styles, so there is no separate CSS file to import.
86
-
87
- ---
88
-
89
- ## 2. Core Concepts & Data Flow
90
-
91
- ### Standard JavaScript `Date` only
92
-
93
- `@mngh/jalali-datepicker` never asks the consumer to construct or parse a custom Jalali object. Every value that crosses the public API boundary — `value`, `defaultValue`, `minDate`, `maxDate`, the `onChange` payload — is a native `Date`, or one of the following shapes depending on `mode`:
94
-
95
- | `mode` | Value shape | Example |
96
- | ------------ | ------------------------------ | ----------------------------------------------- |
97
- | `'single'` | `Date \| null` | `new Date(2026, 2, 21)` |
98
- | `'range'` | `[Date \| null, Date \| null]` | `[new Date(2026, 2, 21), new Date(2026, 3, 1)]` |
99
- | `'multiple'` | `Date[]` | `[new Date(2026, 2, 21), new Date(2026, 5, 1)]` |
100
-
101
- Internally, the picker converts a `Date` to a Jalali year/month/day triple purely for rendering the grid, and converts back to `Date` the instant a value leaves the component. The consumer's state never has to know Jalali math exists.
102
-
103
- ### Under-the-hood math
104
-
105
- The Jalali↔Gregorian conversion is implemented with a self-contained arithmetic algorithm (based on the 33-year leap-year cycle of the Jalali calendar), so there's no dependency on `Intl`, ICU data, or a third-party calendar library. This keeps the bundle small and behavior consistent across browsers and server-side rendering environments.
106
-
107
- ### Digit presentation
108
-
109
- Calendar cells, headers, and the masked text input can render either Persian (`۰-۹`) or Latin (`0-9`) digits:
110
-
111
- ```tsx
112
- <JalaliDatePicker digitType="persian" /> // ۱۴۰۵/۰۱/۰۱
113
- <JalaliDatePicker digitType="latin" /> // 1405/01/01
114
- ```
115
-
116
- `digitType` defaults to `'persian'`.
117
-
118
- ---
119
-
120
- ## 3. Display Variants & Selection Modes
121
-
122
- ### Display variants (`variant`)
123
-
124
- #### `'inline'`
125
-
126
- Renders the calendar permanently in the page flow — ideal for embedding inside a form or sidebar without a trigger input.
127
-
128
- ```tsx
129
- <JalaliDatePicker
130
- variant="inline"
131
- mode="single"
132
- value={date}
133
- onChange={setDate}
134
- />
135
- ```
136
-
137
- #### `'popover'`
138
-
139
- The default — a text input that opens a floating calendar panel on focus/click and closes on outside click or `Escape`.
140
-
141
- ```tsx
142
- <JalaliDatePicker
143
- variant="popover"
144
- mode="single"
145
- value={date}
146
- onChange={setDate}
147
- placeholder="YYYY/MM/DD"
148
- />
149
- ```
150
-
151
- #### `'modal'`
152
-
153
- Opens the calendar in a full-screen dialog with a backdrop blur, scroll locking on `<body>`, and an `Escape` key listener that closes the dialog and returns focus to the trigger.
154
-
155
- ```tsx
156
- <JalaliDatePicker
157
- variant="modal"
158
- mode="single"
159
- value={date}
160
- onChange={setDate}
161
- />
162
- ```
163
-
164
- ### Selection modes (`mode`)
165
-
166
- #### `'single'`
167
-
168
- ```tsx
169
- const [date, setDate] = useState<Date | null>(null);
170
-
171
- <JalaliDatePicker mode="single" value={date} onChange={setDate} />;
172
- ```
173
-
174
- #### `'range'`
175
-
176
- Start/end selection with a live hover preview that highlights the would-be range as the pointer moves before the end date is confirmed.
177
-
178
- ```tsx
179
- const [range, setRange] = useState<[Date | null, Date | null]>([null, null]);
180
-
181
- <JalaliDatePicker
182
- mode="range"
183
- value={range}
184
- onChange={setRange}
185
- numberOfMonths={2}
186
- />;
187
- ```
188
-
189
- #### `'multiple'`
190
-
191
- Select any number of non-contiguous dates; clicking a selected date removes it.
192
-
193
- ```tsx
194
- const [dates, setDates] = useState<Date[]>([]);
195
-
196
- <JalaliDatePicker mode="multiple" value={dates} onChange={setDates} />;
197
- ```
198
-
199
- ---
200
-
201
- ## 4. Built-in Plugins & Advanced Features
202
-
203
- ### Dual month view
204
-
205
- ```tsx
206
- <JalaliDatePicker
207
- mode="range"
208
- numberOfMonths={2}
209
- value={range}
210
- onChange={setRange}
211
- />
212
- ```
213
-
214
- Renders two synchronized month grids side by side (or stacked on narrow viewports), sharing a single hover-preview state — the standard pattern for range pickers.
215
-
216
- ### Time picker integration
217
-
218
- ```tsx
219
- const [date, setDate] = useState<Date | null>(null);
220
- const [time, setTime] = useState<{
221
- hour: number;
222
- minute: number;
223
- second?: number;
224
- }>({
225
- hour: 12,
226
- minute: 0,
227
- });
228
-
229
- <JalaliDatePicker
230
- mode="single"
231
- value={date}
232
- onChange={setDate}
233
- enableTime
234
- timeValue={time}
235
- onTimeChange={setTime}
236
- hourStep={1}
237
- minuteStep={5}
238
- showSeconds={false}
239
- />;
240
- ```
241
-
242
- When `enableTime` is set, the resolved `Date` passed to `onChange` already has the selected hour/minute/second merged in — you don't need to combine `date` and `time` yourself.
243
-
244
- | Prop | Type | Default | Description |
245
- | -------------- | --------------------------------------------------- | ----------- | ----------------------------------------------- |
246
- | `enableTime` | `boolean` | `false` | Shows the time picker panel below the calendar. |
247
- | `timeValue` | `{ hour: number; minute: number; second?: number }` | `undefined` | Controlled time value. |
248
- | `defaultTimeValue` | `{ hour: number; minute: number; second?: number }` | Date/current time | Initial uncontrolled time. |
249
- | `onTimeChange` | `(time) => void` | `undefined` | Fires when the time inputs change. |
250
- | `hourStep` | `number` | `1` | Increment for the hour control. |
251
- | `minuteStep` | `number` | `1` | Increment for the minute control. |
252
- | `secondStep` | `number` | `1` | Increment for the seconds control. |
253
- | `showSeconds` | `boolean` | `false` | Shows a seconds column. |
254
-
255
- ### Live masked input
256
-
257
- ```tsx
258
- <JalaliDatePicker
259
- variant="popover"
260
- mode="single"
261
- value={date}
262
- onChange={setDate}
263
- useMaskedInput
264
- />
265
- ```
266
-
267
- `useMaskedInput` turns the trigger `<input>` into a real-time Persian date mask: digits are inserted into the correct segment as the user types, slashes are auto-inserted, and an invalid segment (e.g. month `13`) is rejected without corrupting the rest of the string.
268
-
269
- ### Iranian solar holidays & Fridays
270
-
271
- ```tsx
272
- <JalaliDatePicker
273
- mode="single"
274
- value={date}
275
- onChange={setDate}
276
- showHolidays
277
- customHolidays={[
278
- {
279
- date: { year: 1405, month: 0, day: 1 },
280
- title: "شرکت تعطیل است",
281
- isOff: true,
282
- },
283
- ]}
284
- />
285
- ```
286
-
287
- When `showHolidays` is enabled, official Iranian solar-calendar holidays and every Friday are rendered in red with a hover/focus tooltip describing the occasion. `customHolidays` merges additional organization-specific dates into the same highlighting and tooltip system.
288
-
289
- ### Calendar events & badges
290
-
291
- ```tsx
292
- <JalaliDatePicker
293
- mode="single"
294
- value={date}
295
- onChange={setDate}
296
- events={[
297
- {
298
- id: "standup",
299
- date: { year: 1405, month: 0, day: 1 },
300
- color: "blue",
301
- title: "Team standup",
302
- },
303
- {
304
- id: "deadline",
305
- date: { year: 1405, month: 0, day: 5 },
306
- color: "red",
307
- title: "Deadline",
308
- },
309
- ]}
310
- />
311
- ```
312
-
313
- Each entry in `events` renders a small colored dot under the corresponding day cell. Its title is included in the cell's accessible label and native tooltip. Up to three event dots are shown per day.
314
-
315
- ### Footer status & action buttons
316
-
317
- ```tsx
318
- <JalaliDatePicker
319
- mode="single"
320
- value={date}
321
- onChange={setDate}
322
- showFooter
323
- showStatusText
324
- showActions
325
- onConfirm={(value) => console.log("confirmed:", value)}
326
- />
327
- ```
328
-
329
- | Prop | Type | Default | Description |
330
- | ---------------- | ----------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
331
- | `showFooter` | `boolean` | `false` | Master switch for the footer row. |
332
- | `showStatusText` | `boolean` | `true` (when footer shown) | Shows a human-readable summary of the current selection, e.g. "5 days selected". |
333
- | `showActions` | `boolean` | `true` (when footer shown) | Shows the **Today**, **Clear**, and **Confirm** buttons. |
334
- | `onConfirm` | `(value) => void` | `undefined` | Fires when **Confirm** is clicked, with the currently pending selection. Useful when you want selection to be provisional until confirmed, especially in `modal` variant. |
335
-
336
- ---
337
-
338
- ## 5. Headless Hook Architecture (`useJalaliDatePicker`)
339
-
340
- For teams that need a completely custom UI — a bespoke calendar layout, a non-standard interaction pattern, or integration into an existing design system — `@mngh/jalali-datepicker` exposes the same state machine that powers its default components as a standalone hook.
341
-
342
- ```tsx
343
- import { useJalaliDatePicker } from "@mngh/jalali-datepicker";
344
-
345
- function CustomCalendar() {
346
- const {
347
- viewYear,
348
- viewMonth,
349
- grid,
350
- goToPrevMonth,
351
- goToNextMonth,
352
- goToToday,
353
- selectDate,
354
- setHoverDate,
355
- clear,
356
- } = useJalaliDatePicker({
357
- mode: "single",
358
- value: null,
359
- onChange: (date) => console.log(date),
360
- });
361
-
362
- return (
363
- <div role="grid" aria-label={`${viewYear}/${viewMonth + 1}`}>
364
- <header>
365
- <button onClick={goToPrevMonth} aria-label="Previous month">
366
- ‹
367
- </button>
368
- <span>
369
- {viewYear}/{viewMonth + 1}
370
- </span>
371
- <button onClick={goToNextMonth} aria-label="Next month">
372
- ›
373
- </button>
374
- </header>
375
-
376
- <div className="grid grid-cols-7">
377
- {grid.map((cell) => (
378
- <button
379
- key={`${cell.jalali.year}-${cell.jalali.month}-${cell.jalali.day}`}
380
- role="gridcell"
381
- disabled={cell.isDisabled || !cell.isCurrentMonth}
382
- aria-selected={cell.isSelected}
383
- data-today={cell.isToday}
384
- onMouseEnter={() => setHoverDate(cell.jalali)}
385
- onMouseLeave={() => setHoverDate(null)}
386
- onClick={() => selectDate(cell.jalali)}
387
- >
388
- {cell.dayNumber}
389
- </button>
390
- ))}
391
- </div>
392
-
393
- <footer>
394
- <button onClick={goToToday}>Today</button>
395
- <button onClick={clear}>Clear</button>
396
- </footer>
397
- </div>
398
- );
399
- }
400
- ```
401
-
402
- ### Hook return values
403
-
404
- | Value | Type | Description |
405
- | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
406
- | `viewYear` | `number` | The Jalali year currently displayed. |
407
- | `viewMonth` | `number` | The Jalali month (0-indexed) currently displayed. |
408
- | `grid` | `JalaliCalendarCell[]` | Flattened 42-cell array, including leading/trailing adjacent-month days. |
409
- | `selected` | `InternalSelectedValue` | Current Jalali selection, shaped according to `mode`. |
410
- | `hoverDate` | `JalaliDate \| null` | Date currently under the pointer, used for range-preview rendering. |
411
- | `goToPrevMonth` / `goToNextMonth` | `() => void` | Step the view by one month. |
412
- | `goToToday` | `() => void` | Reset the view to the month containing today. |
413
- | `setView` | `(year, month) => void` | Jump to a specific Jalali year and zero-based month. |
414
- | `selectDate` | `(date: JalaliDate) => void` | Commit a date into the current selection according to `mode`. |
415
- | `setHoverDate` | `(date: JalaliDate \| null) => void` | Update the hover-preview date (used for range mode). |
416
- | `clear` | `() => void` | Reset the selection to its empty state (`null`, `[null, null]`, or `[]`). |
417
-
418
- ---
419
-
420
- ## 6. Complete API Reference
421
-
422
- ### `JalaliDatePickerProps`
423
-
424
- | Prop | Type | Default | Description |
425
- | -------------------- | -------------------------------------------------------- | ------------------------------ | ----------------------------------------------------------- |
426
- | `variant` | `'inline' \| 'popover' \| 'modal'` | `'inline'` | How the calendar is presented. |
427
- | `mode` | `'single' \| 'range' \| 'multiple'` | `'single'` | Selection strategy. |
428
- | `value` | `Date \| null \| [Date \| null, Date \| null] \| Date[]` | — | Controlled value, shaped by `mode`. |
429
- | `defaultValue` | same as `value` | `null` / `[null, null]` / `[]` | Uncontrolled initial value. |
430
- | `onChange` | `(value) => void` | — | Fires whenever the selection changes. |
431
- | `minDate` | `Date` | `undefined` | Earliest selectable date. |
432
- | `maxDate` | `Date` | `undefined` | Latest selectable date. |
433
- | `isDateDisabled` | `(date: Date) => boolean` | `undefined` | Custom predicate to disable arbitrary dates. |
434
- | `digitType` | `'persian' \| 'latin'` | `'persian'` | Digit glyphs used throughout the UI. |
435
- | `direction` | `'rtl' \| 'ltr'` | `'rtl'` | Layout and horizontal keyboard direction. |
436
- | `firstDayOfWeek` | `0 \| 1 \| ... \| 6` | `0` (Saturday) | First day shown in each week. |
437
- | `initialViewDate` | `Date \| JalaliDate` | selected date / today | Initial visible Jalali month. |
438
- | `numberOfMonths` | `1 \| 2` | `1` | Number of side-by-side month grids. |
439
- | `enableTime` | `boolean` | `false` | Enables the time picker panel. |
440
- | `timeValue` | `JalaliTime \| null` | `undefined` | Controlled time-of-day value; `null` resolves to `00:00`. |
441
- | `defaultTimeValue` | `JalaliTime` | date time / current time | Initial uncontrolled time value. |
442
- | `onTimeChange` | `(time: JalaliTime) => void` | `undefined` | Fires when the time changes. |
443
- | `hourStep` | `number` | `1` | Hour increment step. |
444
- | `minuteStep` | `number` | `1` | Minute increment step. |
445
- | `secondStep` | `number` | `1` | Second increment step. |
446
- | `showSeconds` | `boolean` | `false` | Show a seconds column in the time picker. |
447
- | `useMaskedInput` | `boolean` | `false` | Enables the live typing mask on the trigger input. |
448
- | `showHolidays` | `boolean` | `false` | Highlights official holidays and Fridays. |
449
- | `customHolidays` | `CustomHolidayRule[]` | `[]` | Additional holiday rules to highlight. |
450
- | `events` | `CalendarEvent[]` | `[]` | Event dots/badges rendered on matching day cells. |
451
- | `showFooter` | `boolean` | `false` | Shows the footer row. |
452
- | `showStatusText` | `boolean` | `true` | Shows the selection-summary text in the footer. |
453
- | `showActions` | `boolean` | `true` | Shows Today/Clear/Confirm buttons in the footer. |
454
- | `onConfirm` | `(value) => void` | `undefined` | Fires when Confirm is pressed. |
455
- | `placeholder` | `string` | `'انتخاب تاریخ...'` | Placeholder text for the trigger input (`popover`/`modal`). |
456
- | `closeOnOutsideClick`| `boolean` | `true` | Closes popover/modal when its outside area is clicked. |
457
- | `closeOnEscape` | `boolean` | `true` | Closes popover/modal when Escape is pressed. |
458
- | `onOpenChange` | `(open: boolean) => void` | `undefined` | Reports open-state changes. |
459
- | `className` / `style`| `string` / `CSSProperties` | `undefined` | Root wrapper overrides. |
460
- | `classNames` | `DatePickerClassNames` | `{}` | Per-slot class names. |
461
- | `styles` | `DatePickerStyles` | `{}` | Per-slot inline style overrides. |
462
-
463
- ### Style slots (`classNames` & `styles`)
464
-
465
- Both `classNames` and `styles` accept the same set of slot keys, letting you target any part of the picker with Tailwind classes or inline styles respectively.
466
-
467
- | Slot key | Targets |
468
- | -------- | ------- |
469
- | `root` | Outermost wrapper element. |
470
- | `input`, `inputWrapper`, `inputClearButton` | Trigger input and masked-input controls. |
471
- | `calendar`, `calendarBody`, `calendarPanes`, `calendarPane`, `paneDivider` | Calendar panel and one/two-month layout. |
472
- | `header`, `headerTitle`, `navButton` | Month/year header controls. |
473
- | `weekdays`, `weekdayCell`, `grid` | Weekday row and date grid. |
474
- | `dayCell`, `outsideMonthCell`, `todayCell`, `selectedCell`, `disabledCell`, `holidayCell` | Base and state-specific day cells. |
475
- | `rangeBetweenCell`, `rangeStartCell`, `rangeEndCell` | Range-selection states. |
476
- | `eventBadges`, `eventBadge` | Event indicator wrapper and dots. |
477
- | `monthYearPicker`, `yearList`, `yearButton`, `selectedYearButton` | Year picker elements. |
478
- | `monthGrid`, `monthButton`, `selectedMonthButton` | Month picker elements. |
479
- | `timePicker`, `timeLabel`, `timeControls`, `timeSelect`, `timeSeparator` | Time-picker elements. |
480
- | `footer`, `footerStatus`, `footerActions` | Footer layout and status. |
481
- | `todayButton`, `clearButton`, `confirmButton` | Footer action buttons. |
482
- | `modalBackdrop`, `closeButton` | Modal backdrop and close control. |
483
-
484
- ```tsx
485
- <JalaliDatePicker
486
- classNames={{
487
- root: "font-sans",
488
- input: "rounded-lg border-gray-300 focus:ring-2 focus:ring-blue-500",
489
- selectedCell: "bg-blue-600 text-white",
490
- todayCell: "ring-1 ring-blue-400",
491
- holidayCell: "text-red-500",
492
- }}
493
- />
494
- ```
495
-
496
- ### Exported TypeScript interfaces & types
497
-
498
- ```ts
499
- export type SelectedDateValue =
500
- | Date
501
- | null
502
- | [Date | null, Date | null]
503
- | Date[];
504
-
505
- export type DateRange = [Date | null, Date | null];
506
-
507
- export interface JalaliDate {
508
- year: number;
509
- month: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
510
- day: number;
511
- }
512
-
513
- export interface JalaliTime {
514
- hour: number;
515
- minute: number;
516
- second?: number;
517
- }
518
-
519
- export interface CalendarEvent {
520
- id: string;
521
- date: JalaliDate;
522
- title: string;
523
- description?: string;
524
- color?: string;
525
- }
526
-
527
- export interface CustomHolidayRule {
528
- date: JalaliDate;
529
- title: string;
530
- isOff?: boolean;
531
- }
532
-
533
- export interface JalaliCalendarCell {
534
- jalali: JalaliDate;
535
- gregorianDate: Date;
536
- dayNumber: number;
537
- isCurrentMonth: boolean;
538
- isToday: boolean;
539
- isSelected: boolean;
540
- isDisabled: boolean;
541
- isInRange?: boolean;
542
- isRangeStart?: boolean;
543
- isRangeEnd?: boolean;
544
- }
545
-
546
- export type DatePickerClassNames = DatePickerSlots<string>;
547
- export type DatePickerStyles = DatePickerSlots<React.CSSProperties>;
548
- ```
549
-
550
- ---
551
-
552
- ## 7. Theming & Styling Integration
553
-
554
- ### `DatePickerThemeProvider`
555
-
556
- Wrap your app (or just the picker) in `DatePickerThemeProvider` to switch between light and dark palettes:
557
-
558
- ```tsx
559
- import { DatePickerThemeProvider } from "@mngh/jalali-datepicker";
560
-
561
- <DatePickerThemeProvider mode="dark">
562
- <JalaliDatePicker mode="single" value={date} onChange={setDate} />
563
- </DatePickerThemeProvider>;
564
- ```
565
-
566
- | Prop | Type | Default | Description |
567
- | ------------- | -------------------- | --------- | ---------------------------------------- |
568
- | `mode` | `'light' \| 'dark'` | `'light'` | Selects the built-in color palette. |
569
- | `customTheme` | `DeepPartial<Theme>` | `undefined` | Overrides colors, radii, or shadows. |
570
-
571
- ### Sizing variables
572
-
573
- The root exposes three layout variables. Override them with `style` or `styles.root` to resize the date grid without breaking its column math:
574
-
575
- ```css
576
- .large-picker {
577
- --pdp-cell-size: 42px;
578
- --pdp-cell-gap: 5px;
579
- --pdp-calendar-pane-width: 324px; /* 7 × 42 + 6 × 5 */
580
- }
581
- ```
582
-
583
- Use `customTheme` for palette changes:
584
-
585
- ```tsx
586
- <DatePickerThemeProvider
587
- customTheme={{
588
- colors: { primary: "#0f766e", primaryHover: "#115e59" },
589
- radii: { lg: "12px" },
590
- }}
591
- >
592
- <JalaliDatePicker className="large-picker" />
593
- </DatePickerThemeProvider>
594
- ```
595
-
596
- ### Tailwind CSS via `classNames`
597
-
598
- Every meaningful element has a class slot for Tailwind or regular CSS. Built-in defaults are inline styles, so use `styles` when overriding the same CSS property; use `classNames` for selectors, states, and properties not already set inline.
599
-
600
- ```tsx
601
- <JalaliDatePicker
602
- classNames={{
603
- calendar: "shadow-xl border border-slate-200 rounded-2xl p-4",
604
- header: "flex items-center justify-between mb-2",
605
- dayCell: "h-9 w-9 rounded-full hover:bg-slate-100 transition-colors",
606
- selectedCell: "bg-indigo-600 text-white hover:bg-indigo-600",
607
- }}
608
- />
609
- ```
610
-
611
- ---
612
-
613
- ## 8. Accessibility & Keyboard Shortcuts
614
-
615
- The calendar grid uses `role="grid"` / `role="gridcell"` with a roving `tabindex`. Escape closes popover/modal variants and restores focus to the trigger.
616
-
617
- | Key | Action |
618
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------- |
619
- | `Arrow Left` / `Arrow Right` | Move focus one day (respects the `direction` prop). |
620
- | `Arrow Up` / `Arrow Down` | Move focus one week. |
621
- | `Page Up` / `Page Down` | Move focus to the same day in the previous/next month. |
622
- | `Home` | Move focus to the first day of the current month. |
623
- | `End` | Move focus to the last day of the current month. |
624
- | `Enter` / `Space` | Select the focused date. |
625
- | `Escape` | Close the `popover` or `modal` panel and return focus to the trigger. |
626
- | `Tab` / `Shift + Tab` | Move focus through the available controls in document order. |
627
-
628
- > **Note:** With `direction="rtl"`, `Arrow Left`/`Arrow Right` are swapped automatically so the keys match visual travel.
629
-
630
- > **Tip:** `modal` variant applies `aria-modal="true"` and `role="dialog"`, and locks background scroll via a `overflow: hidden` toggle on `<body>` while open.
1
+ # @mngh/jalali-datepicker
2
+
3
+ A modern, headless-friendly Jalali (Persian/Shamsi) Date & Time Picker for React — zero date-library runtime dependencies, fully typed with native `Date` objects, WAI-ARIA accessible, and themeable with Tailwind CSS or CSS variables.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@mngh/jalali-datepicker.svg)](https://www.npmjs.com/package/@mngh/jalali-datepicker)
6
+ [![bundlephobia](https://img.shields.io/bundlephobia/minzip/@mngh/jalali-datepicker)](https://bundlephobia.com/package/@mngh/jalali-datepicker)
7
+ [![license](https://img.shields.io/npm/l/@mngh/jalali-datepicker.svg)](https://github.com/mngh/jalali-datepicker/blob/main/LICENSE)
8
+ [![types](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://github.com/mngh/jalali-datepicker)
9
+
10
+ > **Repository:** [github.com/mngh/jalali-datepicker](https://github.com/mngh/jalali-datepicker)
11
+ > **Bundle size:** ~38 kB (CJS, unminified) / ~51 kB (ESM, unminified), highly tree-shakeable
12
+ > **Peer dependencies:** React 18 or 19
13
+
14
+ ---
15
+
16
+ ## 🌐 Live Interactive Demo
17
+
18
+ Try all features, props, themes, and picker modes interactively in your browser:
19
+
20
+ 👉 **[Explore Live Demo & Playground](https://mngh-27.github.io/jalali-datepicker-package/)**
21
+
22
+ ## Table of Contents
23
+
24
+ 1. [Introduction & Quick Start](#1-introduction--quick-start)
25
+ 2. [Core Concepts & Data Flow](#2-core-concepts--data-flow)
26
+ 3. [Display Variants & Selection Modes](#3-display-variants--selection-modes)
27
+ 4. [Built-in Plugins & Advanced Features](#4-built-in-plugins--advanced-features)
28
+ 5. [Headless Hook Architecture](#5-headless-hook-architecture-usejalalidatepicker)
29
+ 6. [Complete API Reference](#6-complete-api-reference)
30
+ 7. [Theming & Styling](#7-theming--styling-integration)
31
+ 8. [Accessibility & Keyboard Shortcuts](#8-accessibility--keyboard-shortcuts)
32
+
33
+ ---
34
+
35
+ ## 1. Introduction & Quick Start
36
+
37
+ ### Why `@mngh/jalali-datepicker`?
38
+
39
+ - **Zero date-library overhead** — no Moment.js, no date-fns, no dayjs. The Jalali/Gregorian conversion math is implemented internally with plain arithmetic.
40
+ - **Native `Date` in, native `Date` out** — every prop and callback speaks standard JavaScript `Date` objects. You never touch a custom calendar object.
41
+ - **Three display variants** — inline, popover, and modal — and three selection modes — single, range, and multiple.
42
+ - **Headless-first** — the entire UI is built on top of a public hook (`useJalaliDatePicker`) that you can use to build your own component from scratch.
43
+ - **Accessible by default** — full keyboard navigation, roving tabindex, and ARIA roles (`grid`, `gridcell`, `dialog`) out of the box.
44
+ - **Themeable** — Tailwind CSS class slots and CSS custom properties, with a built-in `DatePickerThemeProvider` for light/dark mode.
45
+
46
+ ### Installation
47
+
48
+ ```bash
49
+ npm install @mngh/jalali-datepicker
50
+ ```
51
+
52
+ ```bash
53
+ pnpm add @mngh/jalali-datepicker
54
+ ```
55
+
56
+ ```bash
57
+ yarn add @mngh/jalali-datepicker
58
+ ```
59
+
60
+ ```bash
61
+ bun add @mngh/jalali-datepicker
62
+ ```
63
+
64
+ ### Quick Start
65
+
66
+ ```tsx
67
+ import { useState } from "react";
68
+ import {
69
+ JalaliDatePicker,
70
+ DatePickerThemeProvider,
71
+ } from "@mngh/jalali-datepicker";
72
+
73
+ export default function App() {
74
+ const [date, setDate] = useState<Date | null>(null);
75
+
76
+ return (
77
+ <DatePickerThemeProvider mode="light">
78
+ <JalaliDatePicker
79
+ variant="popover"
80
+ mode="single"
81
+ value={date}
82
+ onChange={setDate}
83
+ placeholder="Select a date"
84
+ />
85
+ </DatePickerThemeProvider>
86
+ );
87
+ }
88
+ ```
89
+
90
+ That's it — `date` is always a plain JavaScript `Date` object (or `null`). No conversion helpers, no adapters.
91
+ The package ships with self-contained component styles, so there is no separate CSS file to import.
92
+
93
+ ---
94
+
95
+ ## 2. Core Concepts & Data Flow
96
+
97
+ ### Standard JavaScript `Date` only
98
+
99
+ `@mngh/jalali-datepicker` never asks the consumer to construct or parse a custom Jalali object. Every value that crosses the public API boundary — `value`, `defaultValue`, `minDate`, `maxDate`, the `onChange` payload — is a native `Date`, or one of the following shapes depending on `mode`:
100
+
101
+ | `mode` | Value shape | Example |
102
+ | ------------ | ------------------------------ | ----------------------------------------------- |
103
+ | `'single'` | `Date \| null` | `new Date(2026, 2, 21)` |
104
+ | `'range'` | `[Date \| null, Date \| null]` | `[new Date(2026, 2, 21), new Date(2026, 3, 1)]` |
105
+ | `'multiple'` | `Date[]` | `[new Date(2026, 2, 21), new Date(2026, 5, 1)]` |
106
+
107
+ Internally, the picker converts a `Date` to a Jalali year/month/day triple purely for rendering the grid, and converts back to `Date` the instant a value leaves the component. The consumer's state never has to know Jalali math exists.
108
+
109
+ ### Under-the-hood math
110
+
111
+ The Jalali↔Gregorian conversion is implemented with a self-contained arithmetic algorithm (based on the 33-year leap-year cycle of the Jalali calendar), so there's no dependency on `Intl`, ICU data, or a third-party calendar library. This keeps the bundle small and behavior consistent across browsers and server-side rendering environments.
112
+
113
+ ### Digit presentation
114
+
115
+ Calendar cells, headers, and the masked text input can render either Persian (`۰-۹`) or Latin (`0-9`) digits:
116
+
117
+ ```tsx
118
+ <JalaliDatePicker digitType="persian" /> // ۱۴۰۵/۰۱/۰۱
119
+ <JalaliDatePicker digitType="latin" /> // 1405/01/01
120
+ ```
121
+
122
+ `digitType` defaults to `'persian'`.
123
+
124
+ ---
125
+
126
+ ## 3. Display Variants & Selection Modes
127
+
128
+ ### Display variants (`variant`)
129
+
130
+ #### `'inline'`
131
+
132
+ Renders the calendar permanently in the page flow — ideal for embedding inside a form or sidebar without a trigger input.
133
+
134
+ ```tsx
135
+ <JalaliDatePicker
136
+ variant="inline"
137
+ mode="single"
138
+ value={date}
139
+ onChange={setDate}
140
+ />
141
+ ```
142
+
143
+ #### `'popover'`
144
+
145
+ The default — a text input that opens a floating calendar panel on focus/click and closes on outside click or `Escape`.
146
+
147
+ ```tsx
148
+ <JalaliDatePicker
149
+ variant="popover"
150
+ mode="single"
151
+ value={date}
152
+ onChange={setDate}
153
+ placeholder="YYYY/MM/DD"
154
+ />
155
+ ```
156
+
157
+ #### `'modal'`
158
+
159
+ Opens the calendar in a full-screen dialog with a backdrop blur, scroll locking on `<body>`, and an `Escape` key listener that closes the dialog and returns focus to the trigger.
160
+
161
+ ```tsx
162
+ <JalaliDatePicker
163
+ variant="modal"
164
+ mode="single"
165
+ value={date}
166
+ onChange={setDate}
167
+ />
168
+ ```
169
+
170
+ ### Selection modes (`mode`)
171
+
172
+ #### `'single'`
173
+
174
+ ```tsx
175
+ const [date, setDate] = useState<Date | null>(null);
176
+
177
+ <JalaliDatePicker mode="single" value={date} onChange={setDate} />;
178
+ ```
179
+
180
+ #### `'range'`
181
+
182
+ Start/end selection with a live hover preview that highlights the would-be range as the pointer moves before the end date is confirmed.
183
+
184
+ ```tsx
185
+ const [range, setRange] = useState<[Date | null, Date | null]>([null, null]);
186
+
187
+ <JalaliDatePicker
188
+ mode="range"
189
+ value={range}
190
+ onChange={setRange}
191
+ numberOfMonths={2}
192
+ />;
193
+ ```
194
+
195
+ #### `'multiple'`
196
+
197
+ Select any number of non-contiguous dates; clicking a selected date removes it.
198
+
199
+ ```tsx
200
+ const [dates, setDates] = useState<Date[]>([]);
201
+
202
+ <JalaliDatePicker mode="multiple" value={dates} onChange={setDates} />;
203
+ ```
204
+
205
+ ---
206
+
207
+ ## 4. Built-in Plugins & Advanced Features
208
+
209
+ ### Dual month view
210
+
211
+ ```tsx
212
+ <JalaliDatePicker
213
+ mode="range"
214
+ numberOfMonths={2}
215
+ value={range}
216
+ onChange={setRange}
217
+ />
218
+ ```
219
+
220
+ Renders two synchronized month grids side by side (or stacked on narrow viewports), sharing a single hover-preview state — the standard pattern for range pickers.
221
+
222
+ ### Time picker integration
223
+
224
+ ```tsx
225
+ const [date, setDate] = useState<Date | null>(null);
226
+ const [time, setTime] = useState<{
227
+ hour: number;
228
+ minute: number;
229
+ second?: number;
230
+ }>({
231
+ hour: 12,
232
+ minute: 0,
233
+ });
234
+
235
+ <JalaliDatePicker
236
+ mode="single"
237
+ value={date}
238
+ onChange={setDate}
239
+ enableTime
240
+ timeValue={time}
241
+ onTimeChange={setTime}
242
+ hourStep={1}
243
+ minuteStep={5}
244
+ showSeconds={false}
245
+ />;
246
+ ```
247
+
248
+ When `enableTime` is set, the resolved `Date` passed to `onChange` already has the selected hour/minute/second merged in — you don't need to combine `date` and `time` yourself.
249
+
250
+ | Prop | Type | Default | Description |
251
+ | -------------- | --------------------------------------------------- | ----------- | ----------------------------------------------- |
252
+ | `enableTime` | `boolean` | `false` | Shows the time picker panel below the calendar. |
253
+ | `timeValue` | `{ hour: number; minute: number; second?: number }` | `undefined` | Controlled time value. |
254
+ | `defaultTimeValue` | `{ hour: number; minute: number; second?: number }` | Date/current time | Initial uncontrolled time. |
255
+ | `onTimeChange` | `(time) => void` | `undefined` | Fires when the time inputs change. |
256
+ | `hourStep` | `number` | `1` | Increment for the hour control. |
257
+ | `minuteStep` | `number` | `1` | Increment for the minute control. |
258
+ | `secondStep` | `number` | `1` | Increment for the seconds control. |
259
+ | `showSeconds` | `boolean` | `false` | Shows a seconds column. |
260
+
261
+ ### Live masked input
262
+
263
+ ```tsx
264
+ <JalaliDatePicker
265
+ variant="popover"
266
+ mode="single"
267
+ value={date}
268
+ onChange={setDate}
269
+ useMaskedInput
270
+ />
271
+ ```
272
+
273
+ `useMaskedInput` turns the trigger `<input>` into a real-time Persian date mask: digits are inserted into the correct segment as the user types, slashes are auto-inserted, and an invalid segment (e.g. month `13`) is rejected without corrupting the rest of the string.
274
+
275
+ ### Iranian solar holidays & Fridays
276
+
277
+ ```tsx
278
+ <JalaliDatePicker
279
+ mode="single"
280
+ value={date}
281
+ onChange={setDate}
282
+ showHolidays
283
+ customHolidays={[
284
+ {
285
+ date: { year: 1405, month: 0, day: 1 },
286
+ title: "شرکت تعطیل است",
287
+ isOff: true,
288
+ },
289
+ ]}
290
+ />
291
+ ```
292
+
293
+ When `showHolidays` is enabled, official Iranian solar-calendar holidays and every Friday are rendered in red with a hover/focus tooltip describing the occasion. `customHolidays` merges additional organization-specific dates into the same highlighting and tooltip system.
294
+
295
+ ### Calendar events & badges
296
+
297
+ ```tsx
298
+ <JalaliDatePicker
299
+ mode="single"
300
+ value={date}
301
+ onChange={setDate}
302
+ events={[
303
+ {
304
+ id: "standup",
305
+ date: { year: 1405, month: 0, day: 1 },
306
+ color: "blue",
307
+ title: "Team standup",
308
+ },
309
+ {
310
+ id: "deadline",
311
+ date: { year: 1405, month: 0, day: 5 },
312
+ color: "red",
313
+ title: "Deadline",
314
+ },
315
+ ]}
316
+ />
317
+ ```
318
+
319
+ Each entry in `events` renders a small colored dot under the corresponding day cell. Its title is included in the cell's accessible label and native tooltip. Up to three event dots are shown per day.
320
+
321
+ ### Footer status & action buttons
322
+
323
+ ```tsx
324
+ <JalaliDatePicker
325
+ mode="single"
326
+ value={date}
327
+ onChange={setDate}
328
+ showFooter
329
+ showStatusText
330
+ showActions
331
+ onConfirm={(value) => console.log("confirmed:", value)}
332
+ />
333
+ ```
334
+
335
+ | Prop | Type | Default | Description |
336
+ | ---------------- | ----------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | `showFooter` | `boolean` | `false` | Master switch for the footer row. |
338
+ | `showStatusText` | `boolean` | `true` (when footer shown) | Shows a human-readable summary of the current selection, e.g. "5 days selected". |
339
+ | `showActions` | `boolean` | `true` (when footer shown) | Shows the **Today**, **Clear**, and **Confirm** buttons. |
340
+ | `onConfirm` | `(value) => void` | `undefined` | Fires when **Confirm** is clicked, with the currently pending selection. Useful when you want selection to be provisional until confirmed, especially in `modal` variant. |
341
+
342
+ ---
343
+
344
+ ## 5. Headless Hook Architecture (`useJalaliDatePicker`)
345
+
346
+ For teams that need a completely custom UI — a bespoke calendar layout, a non-standard interaction pattern, or integration into an existing design system — `@mngh/jalali-datepicker` exposes the same state machine that powers its default components as a standalone hook.
347
+
348
+ ```tsx
349
+ import { useJalaliDatePicker } from "@mngh/jalali-datepicker";
350
+
351
+ function CustomCalendar() {
352
+ const {
353
+ viewYear,
354
+ viewMonth,
355
+ grid,
356
+ goToPrevMonth,
357
+ goToNextMonth,
358
+ goToToday,
359
+ selectDate,
360
+ setHoverDate,
361
+ clear,
362
+ } = useJalaliDatePicker({
363
+ mode: "single",
364
+ value: null,
365
+ onChange: (date) => console.log(date),
366
+ });
367
+
368
+ return (
369
+ <div role="grid" aria-label={`${viewYear}/${viewMonth + 1}`}>
370
+ <header>
371
+ <button onClick={goToPrevMonth} aria-label="Previous month">
372
+ ‹
373
+ </button>
374
+ <span>
375
+ {viewYear}/{viewMonth + 1}
376
+ </span>
377
+ <button onClick={goToNextMonth} aria-label="Next month">
378
+ ›
379
+ </button>
380
+ </header>
381
+
382
+ <div className="grid grid-cols-7">
383
+ {grid.map((cell) => (
384
+ <button
385
+ key={`${cell.jalali.year}-${cell.jalali.month}-${cell.jalali.day}`}
386
+ role="gridcell"
387
+ disabled={cell.isDisabled || !cell.isCurrentMonth}
388
+ aria-selected={cell.isSelected}
389
+ data-today={cell.isToday}
390
+ onMouseEnter={() => setHoverDate(cell.jalali)}
391
+ onMouseLeave={() => setHoverDate(null)}
392
+ onClick={() => selectDate(cell.jalali)}
393
+ >
394
+ {cell.dayNumber}
395
+ </button>
396
+ ))}
397
+ </div>
398
+
399
+ <footer>
400
+ <button onClick={goToToday}>Today</button>
401
+ <button onClick={clear}>Clear</button>
402
+ </footer>
403
+ </div>
404
+ );
405
+ }
406
+ ```
407
+
408
+ ### Hook return values
409
+
410
+ | Value | Type | Description |
411
+ | --------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
412
+ | `viewYear` | `number` | The Jalali year currently displayed. |
413
+ | `viewMonth` | `number` | The Jalali month (0-indexed) currently displayed. |
414
+ | `grid` | `JalaliCalendarCell[]` | Flattened 42-cell array, including leading/trailing adjacent-month days. |
415
+ | `selected` | `InternalSelectedValue` | Current Jalali selection, shaped according to `mode`. |
416
+ | `hoverDate` | `JalaliDate \| null` | Date currently under the pointer, used for range-preview rendering. |
417
+ | `goToPrevMonth` / `goToNextMonth` | `() => void` | Step the view by one month. |
418
+ | `goToToday` | `() => void` | Reset the view to the month containing today. |
419
+ | `setView` | `(year, month) => void` | Jump to a specific Jalali year and zero-based month. |
420
+ | `selectDate` | `(date: JalaliDate) => void` | Commit a date into the current selection according to `mode`. |
421
+ | `setHoverDate` | `(date: JalaliDate \| null) => void` | Update the hover-preview date (used for range mode). |
422
+ | `clear` | `() => void` | Reset the selection to its empty state (`null`, `[null, null]`, or `[]`). |
423
+
424
+ ---
425
+
426
+ ## 6. Complete API Reference
427
+
428
+ ### `JalaliDatePickerProps`
429
+
430
+ | Prop | Type | Default | Description |
431
+ | -------------------- | -------------------------------------------------------- | ------------------------------ | ----------------------------------------------------------- |
432
+ | `variant` | `'inline' \| 'popover' \| 'modal'` | `'inline'` | How the calendar is presented. |
433
+ | `mode` | `'single' \| 'range' \| 'multiple'` | `'single'` | Selection strategy. |
434
+ | `value` | `Date \| null \| [Date \| null, Date \| null] \| Date[]` | — | Controlled value, shaped by `mode`. |
435
+ | `defaultValue` | same as `value` | `null` / `[null, null]` / `[]` | Uncontrolled initial value. |
436
+ | `onChange` | `(value) => void` | — | Fires whenever the selection changes. |
437
+ | `minDate` | `Date` | `undefined` | Earliest selectable date. |
438
+ | `maxDate` | `Date` | `undefined` | Latest selectable date. |
439
+ | `isDateDisabled` | `(date: Date) => boolean` | `undefined` | Custom predicate to disable arbitrary dates. |
440
+ | `digitType` | `'persian' \| 'latin'` | `'persian'` | Digit glyphs used throughout the UI. |
441
+ | `direction` | `'rtl' \| 'ltr'` | `'rtl'` | Layout and horizontal keyboard direction. |
442
+ | `firstDayOfWeek` | `0 \| 1 \| ... \| 6` | `0` (Saturday) | First day shown in each week. |
443
+ | `initialViewDate` | `Date \| JalaliDate` | selected date / today | Initial visible Jalali month. |
444
+ | `numberOfMonths` | `1 \| 2` | `1` | Number of side-by-side month grids. |
445
+ | `enableTime` | `boolean` | `false` | Enables the time picker panel. |
446
+ | `timeValue` | `JalaliTime \| null` | `undefined` | Controlled time-of-day value; `null` resolves to `00:00`. |
447
+ | `defaultTimeValue` | `JalaliTime` | date time / current time | Initial uncontrolled time value. |
448
+ | `onTimeChange` | `(time: JalaliTime) => void` | `undefined` | Fires when the time changes. |
449
+ | `hourStep` | `number` | `1` | Hour increment step. |
450
+ | `minuteStep` | `number` | `1` | Minute increment step. |
451
+ | `secondStep` | `number` | `1` | Second increment step. |
452
+ | `showSeconds` | `boolean` | `false` | Show a seconds column in the time picker. |
453
+ | `useMaskedInput` | `boolean` | `false` | Enables the live typing mask on the trigger input. |
454
+ | `showHolidays` | `boolean` | `false` | Highlights official holidays and Fridays. |
455
+ | `customHolidays` | `CustomHolidayRule[]` | `[]` | Additional holiday rules to highlight. |
456
+ | `events` | `CalendarEvent[]` | `[]` | Event dots/badges rendered on matching day cells. |
457
+ | `showFooter` | `boolean` | `false` | Shows the footer row. |
458
+ | `showStatusText` | `boolean` | `true` | Shows the selection-summary text in the footer. |
459
+ | `showActions` | `boolean` | `true` | Shows Today/Clear/Confirm buttons in the footer. |
460
+ | `onConfirm` | `(value) => void` | `undefined` | Fires when Confirm is pressed. |
461
+ | `placeholder` | `string` | `'انتخاب تاریخ...'` | Placeholder text for the trigger input (`popover`/`modal`). |
462
+ | `closeOnOutsideClick`| `boolean` | `true` | Closes popover/modal when its outside area is clicked. |
463
+ | `closeOnEscape` | `boolean` | `true` | Closes popover/modal when Escape is pressed. |
464
+ | `onOpenChange` | `(open: boolean) => void` | `undefined` | Reports open-state changes. |
465
+ | `className` / `style`| `string` / `CSSProperties` | `undefined` | Root wrapper overrides. |
466
+ | `classNames` | `DatePickerClassNames` | `{}` | Per-slot class names. |
467
+ | `styles` | `DatePickerStyles` | `{}` | Per-slot inline style overrides. |
468
+
469
+ ### Style slots (`classNames` & `styles`)
470
+
471
+ Both `classNames` and `styles` accept the same set of slot keys, letting you target any part of the picker with Tailwind classes or inline styles respectively.
472
+
473
+ | Slot key | Targets |
474
+ | -------- | ------- |
475
+ | `root` | Outermost wrapper element. |
476
+ | `input`, `inputWrapper`, `inputClearButton` | Trigger input and masked-input controls. |
477
+ | `calendar`, `calendarBody`, `calendarPanes`, `calendarPane`, `paneDivider` | Calendar panel and one/two-month layout. |
478
+ | `header`, `headerTitle`, `navButton` | Month/year header controls. |
479
+ | `weekdays`, `weekdayCell`, `grid` | Weekday row and date grid. |
480
+ | `dayCell`, `outsideMonthCell`, `todayCell`, `selectedCell`, `disabledCell`, `holidayCell` | Base and state-specific day cells. |
481
+ | `rangeBetweenCell`, `rangeStartCell`, `rangeEndCell` | Range-selection states. |
482
+ | `eventBadges`, `eventBadge` | Event indicator wrapper and dots. |
483
+ | `monthYearPicker`, `yearList`, `yearButton`, `selectedYearButton` | Year picker elements. |
484
+ | `monthGrid`, `monthButton`, `selectedMonthButton` | Month picker elements. |
485
+ | `timePicker`, `timeLabel`, `timeControls`, `timeSelect`, `timeSeparator` | Time-picker elements. |
486
+ | `footer`, `footerStatus`, `footerActions` | Footer layout and status. |
487
+ | `todayButton`, `clearButton`, `confirmButton` | Footer action buttons. |
488
+ | `modalBackdrop`, `closeButton` | Modal backdrop and close control. |
489
+
490
+ ```tsx
491
+ <JalaliDatePicker
492
+ classNames={{
493
+ root: "font-sans",
494
+ input: "rounded-lg border-gray-300 focus:ring-2 focus:ring-blue-500",
495
+ selectedCell: "bg-blue-600 text-white",
496
+ todayCell: "ring-1 ring-blue-400",
497
+ holidayCell: "text-red-500",
498
+ }}
499
+ />
500
+ ```
501
+
502
+ ### Exported TypeScript interfaces & types
503
+
504
+ ```ts
505
+ export type SelectedDateValue =
506
+ | Date
507
+ | null
508
+ | [Date | null, Date | null]
509
+ | Date[];
510
+
511
+ export type DateRange = [Date | null, Date | null];
512
+
513
+ export interface JalaliDate {
514
+ year: number;
515
+ month: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
516
+ day: number;
517
+ }
518
+
519
+ export interface JalaliTime {
520
+ hour: number;
521
+ minute: number;
522
+ second?: number;
523
+ }
524
+
525
+ export interface CalendarEvent {
526
+ id: string;
527
+ date: JalaliDate;
528
+ title: string;
529
+ description?: string;
530
+ color?: string;
531
+ }
532
+
533
+ export interface CustomHolidayRule {
534
+ date: JalaliDate;
535
+ title: string;
536
+ isOff?: boolean;
537
+ }
538
+
539
+ export interface JalaliCalendarCell {
540
+ jalali: JalaliDate;
541
+ gregorianDate: Date;
542
+ dayNumber: number;
543
+ isCurrentMonth: boolean;
544
+ isToday: boolean;
545
+ isSelected: boolean;
546
+ isDisabled: boolean;
547
+ isInRange?: boolean;
548
+ isRangeStart?: boolean;
549
+ isRangeEnd?: boolean;
550
+ }
551
+
552
+ export type DatePickerClassNames = DatePickerSlots<string>;
553
+ export type DatePickerStyles = DatePickerSlots<React.CSSProperties>;
554
+ ```
555
+
556
+ ---
557
+
558
+ ## 7. Theming & Styling Integration
559
+
560
+ ### `DatePickerThemeProvider`
561
+
562
+ Wrap your app (or just the picker) in `DatePickerThemeProvider` to switch between light and dark palettes:
563
+
564
+ ```tsx
565
+ import { DatePickerThemeProvider } from "@mngh/jalali-datepicker";
566
+
567
+ <DatePickerThemeProvider mode="dark">
568
+ <JalaliDatePicker mode="single" value={date} onChange={setDate} />
569
+ </DatePickerThemeProvider>;
570
+ ```
571
+
572
+ | Prop | Type | Default | Description |
573
+ | ------------- | -------------------- | --------- | ---------------------------------------- |
574
+ | `mode` | `'light' \| 'dark'` | `'light'` | Selects the built-in color palette. |
575
+ | `customTheme` | `DeepPartial<Theme>` | `undefined` | Overrides colors, radii, or shadows. |
576
+
577
+ ### Sizing variables
578
+
579
+ The root exposes three layout variables. Override them with `style` or `styles.root` to resize the date grid without breaking its column math:
580
+
581
+ ```css
582
+ .large-picker {
583
+ --pdp-cell-size: 42px;
584
+ --pdp-cell-gap: 5px;
585
+ --pdp-calendar-pane-width: 324px; /* 7 × 42 + 6 × 5 */
586
+ }
587
+ ```
588
+
589
+ Use `customTheme` for palette changes:
590
+
591
+ ```tsx
592
+ <DatePickerThemeProvider
593
+ customTheme={{
594
+ colors: { primary: "#0f766e", primaryHover: "#115e59" },
595
+ radii: { lg: "12px" },
596
+ }}
597
+ >
598
+ <JalaliDatePicker className="large-picker" />
599
+ </DatePickerThemeProvider>
600
+ ```
601
+
602
+ ### Tailwind CSS via `classNames`
603
+
604
+ Every meaningful element has a class slot for Tailwind or regular CSS. Built-in defaults are inline styles, so use `styles` when overriding the same CSS property; use `classNames` for selectors, states, and properties not already set inline.
605
+
606
+ ```tsx
607
+ <JalaliDatePicker
608
+ classNames={{
609
+ calendar: "shadow-xl border border-slate-200 rounded-2xl p-4",
610
+ header: "flex items-center justify-between mb-2",
611
+ dayCell: "h-9 w-9 rounded-full hover:bg-slate-100 transition-colors",
612
+ selectedCell: "bg-indigo-600 text-white hover:bg-indigo-600",
613
+ }}
614
+ />
615
+ ```
616
+
617
+ ---
618
+
619
+ ## 8. Accessibility & Keyboard Shortcuts
620
+
621
+ The calendar grid uses `role="grid"` / `role="gridcell"` with a roving `tabindex`. Escape closes popover/modal variants and restores focus to the trigger.
622
+
623
+ | Key | Action |
624
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------- |
625
+ | `Arrow Left` / `Arrow Right` | Move focus one day (respects the `direction` prop). |
626
+ | `Arrow Up` / `Arrow Down` | Move focus one week. |
627
+ | `Page Up` / `Page Down` | Move focus to the same day in the previous/next month. |
628
+ | `Home` | Move focus to the first day of the current month. |
629
+ | `End` | Move focus to the last day of the current month. |
630
+ | `Enter` / `Space` | Select the focused date. |
631
+ | `Escape` | Close the `popover` or `modal` panel and return focus to the trigger. |
632
+ | `Tab` / `Shift + Tab` | Move focus through the available controls in document order. |
633
+
634
+ > **Note:** With `direction="rtl"`, `Arrow Left`/`Arrow Right` are swapped automatically so the keys match visual travel.
635
+
636
+ > **Tip:** `modal` variant applies `aria-modal="true"` and `role="dialog"`, and locks background scroll via a `overflow: hidden` toggle on `<body>` while open.