@keenmate/web-daterangepicker 1.13.0 → 1.14.0-rc01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1062 -1001
  2. package/component-variables.manifest.json +380 -271
  3. package/dist/date-picker-interaction.d.ts +22 -0
  4. package/dist/date-picker-locales.d.ts +24 -0
  5. package/dist/date-picker-navigation.d.ts +70 -0
  6. package/dist/date-picker-rendering.d.ts +95 -0
  7. package/dist/date-picker-selection.d.ts +76 -0
  8. package/dist/date-picker-ui.d.ts +39 -0
  9. package/dist/date-picker-validation.d.ts +65 -0
  10. package/dist/date-picker.d.ts +371 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/logger.d.ts +39 -0
  13. package/dist/modules/click-events/index.d.ts +39 -0
  14. package/dist/modules/index.d.ts +2 -0
  15. package/dist/modules/scroll-events/index.d.ts +27 -0
  16. package/dist/style.css +1 -1
  17. package/dist/tooltip.d.ts +42 -0
  18. package/dist/types.d.ts +537 -0
  19. package/dist/web-component.d.ts +206 -0
  20. package/dist/web-daterangepicker.js +3201 -2354
  21. package/dist/web-daterangepicker.umd.js +89 -33
  22. package/package.json +6 -6
  23. package/src/css/animations.css +15 -0
  24. package/src/css/{_badges.css → badges.css} +7 -7
  25. package/src/css/base.css +171 -0
  26. package/src/css/{_calendar-grid.css → calendar-grid.css} +77 -54
  27. package/src/css/clock-picker.css +222 -0
  28. package/src/css/compact-picker.css +141 -0
  29. package/src/css/controls.css +122 -0
  30. package/src/css/dark-mode.css +60 -0
  31. package/src/css/{_tooltips.css → floating.css} +35 -37
  32. package/src/css/{_header-navigation.css → header-navigation.css} +54 -54
  33. package/src/css/{_loading.css → loading.css} +4 -13
  34. package/src/css/main.css +41 -27
  35. package/src/css/{_message.css → message.css} +15 -15
  36. package/src/css/{_modal.css → modal.css} +25 -25
  37. package/src/css/{_modifiers.css → states.css} +19 -15
  38. package/src/css/{_summary-actions.css → summary-actions.css} +19 -18
  39. package/src/css/time-picker.css +180 -0
  40. package/src/css/{_variables.css → variables.css} +219 -30
  41. package/src/css/wheel-picker.css +195 -0
  42. package/src/css/_base.css +0 -285
package/README.md CHANGED
@@ -1,1001 +1,1062 @@
1
- # Date Range Picker Web Component
2
-
3
- A lightweight, accessible date picker web component with excellent keyboard navigation and range selection support.
4
-
5
- > **⚠️ Security Notice:** This component intentionally allows raw HTML in rendering callbacks and message content to give developers full control over content display. If you display user-generated content, you must sanitize it yourself. See [HTML Injection (XSS) Notice](#html-injection-xss-notice) for the complete list of affected callbacks and methods.
6
-
7
- ## Features
8
-
9
- - 🎯 **Input Masking** - Auto-format dates as you type with separator insertion
10
- - ⌨️ **Keyboard Navigation** - Full keyboard support (arrows, Enter, Esc, PageUp/Down, Home/End)
11
- - 📅 **Rolling Selector** - Innovative scrollable year/month picker
12
- - 📊 **Multi-Month Display** - Show 1-3+ months side by side with independent navigation
13
- - 🎨 **Themeable** - All styles use CSS custom properties (`--drp-*`)
14
- - 🖱️ **Drag-to-Adjust** - Drag range endpoints to adjust selection (range mode)
15
- - 👀 **Live Hover Preview** - See the would-be range painted live as you move toward an end date (range mode, mode-aware per `disabled-dates-handling`)
16
- - 🌐 **Multiple Formats** - YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY, etc.
17
- - 🌍 **Locale-Aware** - Auto-detect week start day from user's locale
18
- - 🚫 **Date Restrictions** - Min/max dates, disabled days/dates, custom disable logic
19
- - 🎉 **Special Dates** - Highlight holidays, events with custom labels and styling
20
- - **Modern** - Web Component with Shadow DOM, TypeScript, bundled with Vite
21
-
22
- ## Installation
23
-
24
- ```bash
25
- npm install @keenmate/web-daterangepicker
26
- ```
27
-
28
- ## Usage
29
-
30
- ### Basic HTML
31
-
32
- ```html
33
- <!-- Single date picker -->
34
- <web-daterangepicker
35
- selection-mode="single"
36
- date-format-mask="YYYY-MM-DD"
37
- placeholder="Select date"
38
- ></web-daterangepicker>
39
-
40
- <!-- Date range picker -->
41
- <web-daterangepicker
42
- selection-mode="range"
43
- date-format-mask="YYYY-MM-DD"
44
- visible-months-count="2"
45
- placeholder="Select date range"
46
- ></web-daterangepicker>
47
- ```
48
-
49
- ### With JavaScript/TypeScript
50
-
51
- ```typescript
52
- // Import the component (includes styles)
53
- import '@keenmate/web-daterangepicker';
54
-
55
- // Or import styles separately if needed
56
- import '@keenmate/web-daterangepicker/style.css';
57
-
58
- const picker = document.querySelector('web-daterangepicker');
59
-
60
- // Listen for date selection
61
- picker.addEventListener('date-select', (e) => {
62
- console.log('Selected:', e.detail.formattedValue);
63
- console.log('Date object:', e.detail.date);
64
- console.log('Range:', e.detail.dateRange);
65
- });
66
-
67
- // Programmatic API
68
- picker.show(); // Show calendar
69
- picker.hide(); // Hide calendar
70
- picker.toggle(); // Toggle calendar
71
- picker.clearSelection(); // Clear selection
72
- picker.getInputValue(); // Get current value
73
- picker.setInputValue('2025-11-15'); // Set value
74
- ```
75
-
76
- ### JavaScript Instantiation (Direct Class Usage)
77
-
78
- If you prefer to use the `DateRangePicker` class directly instead of the web component, you have full programmatic control:
79
-
80
- ```typescript
81
- import { DateRangePicker } from '@keenmate/web-daterangepicker';
82
- // IMPORTANT: Import CSS separately (web component auto-injects, but the class doesn't)
83
- import '@keenmate/web-daterangepicker/dist/style.css';
84
-
85
- const inputElement = document.getElementById('my-input');
86
-
87
- const picker = new DateRangePicker(inputElement, {
88
- selectionMode: 'range',
89
- visibleMonthsCount: 2,
90
- dateFormatMask: 'YYYY-MM-DD',
91
- onSelect: (detail) => {
92
- // Range mode: detail is { start: Date, end: Date }
93
- console.log('Range:', detail.start, 'to', detail.end);
94
- }
95
- });
96
- ```
97
-
98
- **⚠️ Critical: CSS Requirements**
99
-
100
- Unlike the web component, the `DateRangePicker` class does **NOT** automatically inject styles. You **MUST** import CSS separately or you'll see an unstyled calendar. Choose one method:
101
-
102
- **Option 1: Import CSS in JavaScript (Recommended)**
103
- ```typescript
104
- import { DateRangePicker } from '@keenmate/web-daterangepicker';
105
- import '@keenmate/web-daterangepicker/dist/style.css';
106
- ```
107
-
108
- **Option 2: Link CSS in HTML**
109
- ```html
110
- <link rel="stylesheet" href="./node_modules/@keenmate/web-daterangepicker/dist/style.css">
111
- ```
112
-
113
- **Option 3: Programmatic Injection**
114
- ```typescript
115
- import { DateRangePicker } from '@keenmate/web-daterangepicker';
116
-
117
- // Inject styles once before creating pickers
118
- DateRangePicker.injectGlobalStyles();
119
-
120
- const picker = new DateRangePicker(inputElement, options);
121
- ```
122
-
123
- **Why?** The web component uses Shadow DOM and injects styles into its isolated scope automatically. The `DateRangePicker` class creates calendar elements in the regular DOM, so it expects global CSS to be loaded separately.
124
-
125
- See the [JavaScript Instantiation Examples](examples-javascript-instantiation.html) for complete code samples and configuration options.
126
-
127
- ## Attributes
128
-
129
- > **Note:** HTML attributes use kebab-case (e.g., `selection-mode`), while JavaScript options use camelCase (e.g., `selectionMode`).
130
-
131
- | Attribute | Type | Default | Description |
132
- |-----------|------|---------|-------------|
133
- | `selection-mode` | `'single' \| 'range'` | `'single'` | Single date or date range selection |
134
- | `date-format-mask` | `string` | `'YYYY-MM-DD'` | Date format (YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY, etc.) |
135
- | `visible-months-count` | `number` | `1` (single), `2` (range) | Number of months to display |
136
- | `calendar-open-trigger` | `'focus' \| 'typing' \| 'manual'` | `'focus'` | How to open calendar (focus = on input focus, typing = when user types, manual = programmatic only) |
137
- | `value` | `string` | - | Current value |
138
- | `placeholder` | `string` | - | Input placeholder text |
139
- | `disabled` | `boolean` | `false` | Disable the picker |
140
- | `week-start-day` | `'auto' \| 0-6` | `'auto'` | First day of week (0=Sunday, 1=Monday, etc. 'auto'=detect from locale) |
141
- | `min-date` | `string` | - | Minimum selectable date (YYYY-MM-DD) |
142
- | `max-date` | `string` | - | Maximum selectable date (YYYY-MM-DD) |
143
- | `disabled-weekdays` | `string` | - | Comma-separated day numbers to disable (e.g., "0,6" for weekends) |
144
- | `disabled-dates` | `string` | - | Comma-separated ISO dates to disable (e.g., `"2026-06-13, 2026-06-14, 2026-12-25"`). The `disabledDates` property still wins if both paths are set. |
145
- | `disabled-dates-handling` | `'allow' \| 'prevent' \| 'block' \| 'split' \| 'individual'` | `'allow'` | How to handle range selections over disabled dates (see [Range Selection Modes](#range-selection-modes)) |
146
- | `display-format-mask` | `string` | Same as `date-format-mask` | Localized format hint shown to users (e.g., `'dd/mm/aaaa'` Spanish, `'tt.mm.jjjj'` German). Used as the input placeholder when no explicit `placeholder` is set. Validation still uses `date-format-mask`. |
147
- | `highlight-disabled-in-range` | `boolean` | `true` | Whether to visually highlight disabled dates within a selected range. Set to `false` to only highlight enabled dates. |
148
- | `auto-close` | `'never' \| 'selection' \| 'apply'` | `'selection'` | When to close calendar (selection = after picking, apply = after Apply button, never = manual) |
149
- | `positioning-mode` | `'inline' \| 'floating' \| 'modal'` | `'floating'` | Calendar positioning (inline = embedded, floating = popup anchored to input, modal = centered overlay with backdrop) |
150
- | `mobile-modal-breakpoint` | CSS length (e.g., `"640px"`, `"40em"`) | | When configured mode is `floating`, auto-switch to `modal` below this viewport width. |
151
- | `mobile-modal-min-height` | CSS length | — | Same idea but for viewport height — modal engages when viewport height is below the threshold. ORs with `mobile-modal-breakpoint`. |
152
- | `show-summary` | `boolean` | `true` | Show range-mode days/nights summary block. Set to `false` to omit entirely (no empty-div jump). |
153
- | `input-size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Input field size (floating/modal modes only) |
154
- | `enable-transitions` | `boolean` | `false` | Enable CSS transitions/animations |
155
- | `date-member` | `string` | `'date'` | Field name on `specialDates` objects holding the date. Lets you reuse existing data shapes without renaming keys. |
156
- | `badge-text-member` | `string` | `'badgeText'` | Field name on `specialDates` objects holding the badge label. |
157
- | `badge-class-member` | `string` | `'badgeClass'` | Field name on `specialDates` objects holding the badge's extra CSS class. |
158
- | `day-class-member` | `string` | `'dayClass'` | Field name on `specialDates` objects holding the day cell's extra CSS class. |
159
- | `badge-tooltip-member` | `string` | `'badgeTooltip'` | Field name on `specialDates` objects holding the badge tooltip text. |
160
- | `day-tooltip-member` | `string` | `'dayTooltip'` | Field name on `specialDates` objects holding the day tooltip text. |
161
- | `is-disabled-member` | `string` | `'isDisabled'` | Field name on `specialDates` objects holding the disabled flag. |
162
-
163
- ## Properties
164
-
165
- ```typescript
166
- // Get/set properties (use camelCase in JavaScript)
167
- picker.selectionMode = 'range';
168
- picker.dateFormatMask = 'DD.MM.YYYY';
169
- picker.value = '2025-11-15';
170
- picker.disabled = true;
171
-
172
- // Complex data (arrays / objects / callbacks) set via property, not attribute
173
- picker.disabledDates = ['2026-06-13', '2026-06-14'];
174
- picker.specialDates = [{ date: '2026-12-25', badgeText: '🎄', badgeTooltip: 'Christmas' }];
175
-
176
- // Localization overrides
177
- picker.customStrings = { today: 'Jump', clear: 'Wipe' };
178
- picker.monthNames = ['01','02','03','04','05','06','07','08','09','10','11','12'];
179
- ```
180
-
181
- > Property setters work even before the element is upgraded — assignments made before `customElements.define()` runs are routed through the accessors during `connectedCallback`, so you don't need `customElements.whenDefined('web-daterangepicker')` guards.
182
-
183
- ## Methods
184
-
185
- | Method | Description |
186
- |--------|-------------|
187
- | `show()` | Show the calendar (floating mode only) |
188
- | `hide()` | Hide the calendar |
189
- | `toggle()` | Toggle calendar visibility |
190
- | `clearSelection()` | Clear the current selection |
191
- | `getInputValue()` | Get the current value as a string |
192
- | `setInputValue(value: string)` | Set the value |
193
- | `showMessage(html: string)` | Display a message in the calendar with custom HTML content |
194
- | `hideMessage()` | Hide the currently displayed message |
195
-
196
- ## Events
197
-
198
- | Event | Detail | Description |
199
- |-------|--------|-------------|
200
- | `date-select` | `{ date?, dateRange?, formattedValue }` | Fired when a date is selected |
201
- | `change` | `{ date?, dateRange?, formattedValue }` | Fired when selection changes |
202
- | `custom-action` | `{ [key: string]: string }` | Fired when a button with `data-action="custom"` is clicked. Detail contains all `data-*` attributes as camelCase keys. |
203
-
204
- > There are no separate `apply` or `cancel` events. The Apply button commits the pending selection and dispatches `change`. Pressing Escape with an uncommitted selection silently restores the input value and fires nothing.
205
-
206
- ## Keyboard Shortcuts
207
-
208
- - **↑ ↓** - Navigate up/down by week
209
- - **← →** - Navigate left/right by day
210
- - **Ctrl+← / Ctrl+→** - Previous / next month (maintains day position)
211
- - **PageUp / PageDown** - Previous / next month (maintains day position)
212
- - **Home** - First day of current month (repeat to cycle backwards through months)
213
- - **End** - Last day of current month (repeat to cycle forwards through months)
214
- - **Ctrl+Home** - January 1st of current year (repeat for previous year)
215
- - **Ctrl+End** - December 31st of current year (repeat for next year)
216
- - **Enter** - Select focused day
217
- - **Escape** - Close calendar
218
- - **Tab / Shift+Tab** - Switch between month columns (multi-month mode)
219
- - **T** - Jump to today
220
-
221
- ## ⚠️ Working with Dates Across Timezones
222
-
223
- This is a **calendar-date picker** — it represents days, not moments in time. Skipping this section will eventually cost you a one-day-shift bug, so please read it.
224
-
225
- ### How the picker represents dates
226
-
227
- When the user clicks April 30, the picker stores `new Date(year, month - 1, day)` that's **local midnight** on the picked day. The same is true for `selectedDate`, `selectedStartDate`, `selectedEndDate`, and the Date passed to every callback. There's no UTC anywhere in the picker's internal lookup paths.
228
-
229
- ### The trap: `Date.prototype.toISOString()`
230
-
231
- `toISOString()` returns the date in **UTC**. For any user not in UTC, that string represents a different *calendar* day than the one they see:
232
-
233
- ```js
234
- // User in Moscow (UTC+3) picks April 30.
235
- // selectedDate is `new Date(2026, 3, 30)` = April 30 00:00 MSK = April 29 21:00 UTC.
236
-
237
- selectedDate.toISOString().split('T')[0] // "2026-04-29" ❌ wrong day
238
- selectedDate.getDate() // 30 ✅ what the user clicked
239
- ```
240
-
241
- This is what broke the example demos in earlier versions: lookup keys built from `dayOffset()` (local) didn't match keys derived from `toISOString()` (UTC). Badges rendered one day late, tooltips on the right day. Same bug strikes any user who tries `date.toISOString()` to key into a `Map<string, DateInfo>`.
242
-
243
- ### The rule
244
-
245
- **Format dates from local components, never from `toISOString()`:**
246
-
247
- ```js
248
- const toLocalISO = (d) =>
249
- `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
250
-
251
- picker.getDateMetadataCallback = (date) => {
252
- const dateStr = toLocalISO(date); // matches what the user sees
253
- // …lookup, return DateInfo…
254
- };
255
- ```
256
-
257
- ### Sending dates to a server / between users
258
-
259
- `Date` objects can't survive transport — they get serialized. The serialization format determines whether the date stays on the right calendar day across timezones:
260
-
261
- | What you send | Russian user picks Apr 30 → LA user receives | Verdict |
262
- |---|---|---|
263
- | `event.detail.formattedValue` (`"2026-04-30"`) | `"2026-04-30"` → set on LA picker → April 30 | ✅ |
264
- | `JSON.stringify(detail)` (DateISO string) | `"2026-04-29T21:00:00.000Z"` April 29 in LA | ❌ |
265
- | `selectedDate.toISOString()` | `"2026-04-29T21:00:00.000Z"` April 29 in LA | ❌ |
266
- | `toLocalISO(selectedDate)` (`"2026-04-30"`) | `"2026-04-30"` | ✅ |
267
-
268
- **Recommendation:** transmit calendar dates as `YYYY-MM-DD` **strings**, never as ISO timestamps or raw `Date`/JSON-serialized payloads.
269
-
270
- ### Receiving a date string
271
-
272
- `new Date("2026-04-30")` parses as **UTC midnight** surprising for non-UTC users (in LA it'd be April 29). Build local Dates from the string parts:
273
-
274
- ```js
275
- // Don't do this
276
- const d = new Date("2026-04-30"); // UTC midnight, surprising in non-UTC
277
-
278
- // Do this
279
- const [y, m, day] = "2026-04-30".split("-").map(Number);
280
- const localDate = new Date(y, m - 1, day); // April 30 local — what the user picked
281
-
282
- // Or, if you just need to set the picker:
283
- picker.setInputValue("2026-04-30"); // picker handles parsing internally
284
- picker.value = "2026-04-30"; // attribute setter
285
- ```
286
-
287
- ### Quick checklist
288
-
289
- - [ ] Picker callbacks: format Dates with `toLocalISO()`, not `toISOString()`
290
- - [ ] Outgoing dates (server, URL, localStorage): send `YYYY-MM-DD` strings
291
- - [ ] Incoming dates: prefer `picker.setInputValue("YYYY-MM-DD")` over manual `new Date(...)` parsing
292
- - [ ] Map/dictionary keys for date lookups: build keys with `toLocalISO()` and lookup the same way
293
-
294
- ## Advanced Features
295
-
296
- ### Week Start Day
297
-
298
- Control which day the week starts on (auto-detected by default from user's locale):
299
-
300
- ```html
301
- <!-- Auto-detect from locale (default) -->
302
- <web-daterangepicker week-start-day="auto"></web-daterangepicker>
303
-
304
- <!-- Force Sunday start -->
305
- <web-daterangepicker week-start-day="0"></web-daterangepicker>
306
-
307
- <!-- Force Monday start (common in Europe) -->
308
- <web-daterangepicker week-start-day="1"></web-daterangepicker>
309
- ```
310
-
311
- ### Disabled Dates & Date Restrictions
312
-
313
- #### Simple Restrictions (Attributes)
314
-
315
- ```html
316
- <!-- Disable weekends -->
317
- <web-daterangepicker disabled-weekdays="0,6"></web-daterangepicker>
318
-
319
- <!-- Date range restriction -->
320
- <web-daterangepicker
321
- min-date="2025-01-01"
322
- max-date="2025-12-31">
323
- </web-daterangepicker>
324
- ```
325
-
326
- #### Complex Restrictions (JavaScript)
327
-
328
- ```javascript
329
- const picker = document.querySelector('web-daterangepicker');
330
-
331
- // Disable specific dates (e.g., public holidays)
332
- picker.disabledDates = [
333
- '2025-12-25', // Christmas
334
- '2025-01-01', // New Year
335
- new Date(2025, 6, 4) // July 4th
336
- ];
337
-
338
- // Custom disable logic (e.g., cottage booking)
339
- picker.getDateMetadataCallback = (date) => {
340
- // Disable all dates that overlap with existing bookings
341
- const isBooked = bookedRanges.some(range =>
342
- date >= range.start && date <= range.end
343
- );
344
- return isBooked ? { isDisabled: true } : null;
345
- };
346
- ```
347
-
348
- ### Special Dates (Holidays, Events)
349
-
350
- Add visual indicators and labels to specific dates:
351
-
352
- ```javascript
353
- const picker = document.querySelector('web-daterangepicker');
354
-
355
- picker.specialDates = [
356
- {
357
- date: '2025-12-25',
358
- dayClass: 'holiday', // CSS class for the day cell
359
- badgeText: '🎄', // Badge overlay (emoji or short text)
360
- dayTooltip: 'Christmas Day'
361
- },
362
- {
363
- date: '2025-07-04',
364
- dayClass: 'holiday',
365
- badgeText: '🎆',
366
- dayTooltip: 'Independence Day'
367
- },
368
- {
369
- date: '2025-02-14',
370
- dayClass: 'event',
371
- badgeText: '❤️',
372
- dayTooltip: 'Valentine\'s Day'
373
- }
374
- ];
375
- ```
376
-
377
- ### Advanced Styling & Info
378
-
379
- For complete control, use the `getDateMetadataCallback`:
380
-
381
- ```javascript
382
- // Local-date formatter — see "Working with Dates Across Timezones" above.
383
- // Don't use date.toISOString() here; it shifts by one day in non-UTC timezones.
384
- const toLocalISO = (d) =>
385
- `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
386
-
387
- picker.getDateMetadataCallback = (date) => {
388
- const dateStr = toLocalISO(date);
389
-
390
- // Check if it's a peak season date
391
- if (isPeakSeason(date)) {
392
- return {
393
- dayClass: 'peak-season',
394
- badgeText: '$$$',
395
- dayTooltip: 'Peak season pricing'
396
- };
397
- }
398
-
399
- // Check if it's a special offer date
400
- if (specialOffers[dateStr]) {
401
- return {
402
- dayClass: 'special-offer',
403
- badgeText: '%',
404
- dayTooltip: `${specialOffers[dateStr]}% off!`
405
- };
406
- }
407
-
408
- return null;
409
- };
410
- ```
411
-
412
- ### CSS Styling for Special Dates
413
-
414
- Since the component uses Shadow DOM, inject custom styles via the `customStylesCallback`:
415
-
416
- ```javascript
417
- picker.customStylesCallback = () => `
418
- /* Holiday styling */
419
- .drp-date-picker__day.holiday {
420
- background-color: rgba(239, 68, 68, 0.1);
421
- }
422
-
423
- /* Event styling */
424
- .drp-date-picker__day.event {
425
- background-color: rgba(16, 185, 129, 0.1);
426
- }
427
-
428
- /* Custom class example */
429
- .drp-date-picker__day.peak-season {
430
- background-color: rgba(251, 191, 36, 0.15);
431
- font-weight: 600;
432
- }
433
- `;
434
- ```
435
-
436
- ### Date Selection Validation (beforeDateSelectCallback)
437
-
438
- Validate or modify date selections before they're applied. Supports async validation (e.g., API calls):
439
-
440
- ```javascript
441
- const picker = document.querySelector('web-daterangepicker');
442
-
443
- picker.beforeDateSelectCallback = async (selection) => {
444
- // selection is Date (single mode) or { start: Date, end: Date } (range mode)
445
-
446
- // Example: Check availability via API.
447
- // Send YYYY-MM-DD strings, not toISOString() — the picker is a calendar-date
448
- // picker, and ISO timestamps shift by ±1 day across timezones.
449
- const response = await fetch('/api/check-availability', {
450
- method: 'POST',
451
- body: JSON.stringify({
452
- start: toLocalISO(selection.start),
453
- end: toLocalISO(selection.end)
454
- })
455
- });
456
- const { available, message } = await response.json();
457
-
458
- if (!available) {
459
- return {
460
- action: 'restore',
461
- message: message,
462
- showInvalidRange: true // Keep selection visible with error styling
463
- };
464
- }
465
-
466
- return { action: 'accept' };
467
- };
468
- ```
469
-
470
- **Return object options:**
471
-
472
- | Property | Type | Description |
473
- |----------|------|-------------|
474
- | `action` | `'accept' \| 'adjust' \| 'restore' \| 'clear'` | **Required.** What to do with the selection |
475
- | `message` | `string` | Optional message to display in the calendar |
476
- | `showInvalidRange` | `boolean` | When `true` with `action: 'restore'`, keeps the invalid selection visible with red error styling |
477
- | `adjustedDate` | `Date` | For `action: 'adjust'` in single mode - the corrected date |
478
- | `adjustedStartDate` | `Date` | For `action: 'adjust'` in range mode - the corrected start date |
479
- | `adjustedEndDate` | `Date` | For `action: 'adjust'` in range mode - the corrected end date |
480
-
481
- **Action behaviors:**
482
-
483
- - **`accept`**: Apply the selection as-is. Hides any existing message.
484
- - **`adjust`**: Apply corrected dates instead (use with `adjustedDate` or `adjustedStartDate`/`adjustedEndDate`)
485
- - **`restore`**: Revert to previous selection. Use `showInvalidRange: true` to show what was attempted.
486
- - **`clear`**: Clear the selection entirely.
487
-
488
- **Example: Minimum nights validation with error display:**
489
-
490
- ```javascript
491
- picker.beforeDateSelectCallback = (range) => {
492
- const nights = Math.floor((range.end - range.start) / (1000 * 60 * 60 * 24));
493
-
494
- if (nights < 2) {
495
- return {
496
- action: 'restore',
497
- message: 'Minimum 2 nights required',
498
- showInvalidRange: true // Shows attempted range with red styling
499
- };
500
- }
501
-
502
- if (nights > 14) {
503
- return {
504
- action: 'restore',
505
- message: 'Maximum 14 nights allowed',
506
- showInvalidRange: true
507
- };
508
- }
509
-
510
- return { action: 'accept' };
511
- };
512
- ```
513
-
514
- ### Bulk Metadata Loading (beforeMonthChangedCallback)
515
-
516
- Load metadata for all visible dates in a single API call when the user navigates months. Much more efficient than `getDateMetadataCallback` which is called per-date:
517
-
518
- ```javascript
519
- const picker = document.querySelector('web-daterangepicker');
520
-
521
- picker.beforeMonthChangedCallback = async (context) => {
522
- // context: { year, month, monthIndex, firstVisibleDate, lastVisibleDate }
523
-
524
- // Fetch availability for all visible dates in one call.
525
- // Use toLocalISO (see "Working with Dates Across Timezones") so the
526
- // server receives the calendar dates the user actually sees.
527
- const response = await fetch('/api/availability', {
528
- method: 'POST',
529
- body: JSON.stringify({
530
- start: toLocalISO(context.firstVisibleDate),
531
- end: toLocalISO(context.lastVisibleDate)
532
- })
533
- });
534
- const data = await response.json();
535
-
536
- // Build metadata map (key: YYYY-MM-DD, value: DateInfo)
537
- const metadata = new Map();
538
- data.forEach(day => {
539
- metadata.set(day.date, {
540
- badgeText: `$${day.price}`,
541
- isDisabled: !day.available,
542
- dayTooltip: `${day.roomsLeft} rooms available`
543
- });
544
- });
545
-
546
- return { action: 'accept', metadata };
547
- };
548
- ```
549
-
550
- **Return object options:**
551
-
552
- | Property | Type | Description |
553
- |----------|------|-------------|
554
- | `action` | `'accept' \| 'block'` | **Required.** Allow or prevent month navigation |
555
- | `metadata` | `Map<string, DateInfo>` | Bulk metadata keyed by `YYYY-MM-DD`. Cached and used instead of `getDateMetadataCallback` |
556
- | `monthHeaders` | `Map<string, string>` | Custom month headers keyed by `YYYY-MM` (e.g., `"2026-01"` → `"Jan 2026 (5 rooms)"`) |
557
- | `message` | `string` | Optional message to display (useful with `action: 'block'`) |
558
-
559
- **Performance:** 1 API call per month navigation vs 35-42 calls with `getDateMetadataCallback`.
560
-
561
- ### Messages & Custom Actions
562
-
563
- Display contextual messages in the calendar with interactive buttons:
564
-
565
- ```javascript
566
- const picker = document.querySelector('web-daterangepicker');
567
-
568
- // Show a simple message
569
- picker.showMessage('<p>Please select a check-in date</p>');
570
-
571
- // Show a message with a close button
572
- picker.showMessage(`
573
- <p>Weekend dates have higher rates</p>
574
- <button data-action="close-message">Got it</button>
575
- `);
576
-
577
- // Show a message with custom action buttons
578
- picker.showMessage(`
579
- <p>These dates are unavailable. Try:</p>
580
- <button data-action="custom" data-start-date="2026-01-14" data-end-date="2026-01-17">
581
- Jan 14 - Jan 17
582
- </button>
583
- `);
584
-
585
- // Handle custom action button clicks
586
- picker.addEventListener('custom-action', (e) => {
587
- const { startDate, endDate } = e.detail;
588
- if (startDate && endDate) {
589
- picker.selectedRanges = [{
590
- start: new Date(startDate),
591
- end: new Date(endDate)
592
- }];
593
- picker.hideMessage();
594
- }
595
- });
596
-
597
- // Hide message programmatically
598
- picker.hideMessage();
599
- ```
600
-
601
- **Built-in button actions:**
602
- - `data-action="close-message"` - Closes the message (no event fired)
603
- - `data-action="custom"` - Fires `custom-action` event with all `data-*` attributes
604
-
605
- ## Range Selection Modes
606
-
607
- When selecting date ranges that include disabled dates (e.g., selecting a working week where weekends are disabled), you can control how the selection is handled using the `disabled-dates-handling` attribute:
608
-
609
- ### Mode: 'allow' (default)
610
-
611
- Allows range selections over disabled dates. Returns both enabled and disabled date arrays:
612
-
613
- ```html
614
- <web-daterangepicker
615
- selection-mode="range"
616
- disabled-weekdays="0,6"
617
- disabled-dates-handling="allow">
618
- </web-daterangepicker>
619
-
620
- <script>
621
- picker.addEventListener('date-select', (e) => {
622
- console.log('Enabled dates:', e.detail.enabledDates);
623
- console.log('Disabled dates:', e.detail.disabledDates);
624
- console.log('Total days:', e.detail.getTotalDays());
625
- console.log('Enabled count:', e.detail.getEnabledDateCount());
626
- });
627
- </script>
628
- ```
629
-
630
- **Use case:** Selecting working weeks where you need to know both working days and weekends (e.g., "Select 3 weeks of work" where weekends are included in the range but you get a separate array of working days).
631
-
632
- ### Mode: 'prevent'
633
-
634
- Prevents selecting disabled dates entirely. Clicking a disabled date does nothing:
635
-
636
- ```html
637
- <web-daterangepicker
638
- selection-mode="range"
639
- disabled-dates-handling="prevent">
640
- </web-daterangepicker>
641
- ```
642
-
643
- **Use case:** Strict date selection where disabled dates should never be part of any selection.
644
-
645
- ### Mode: 'block'
646
-
647
- Prevents range selections from crossing disabled dates. Automatically snaps to the last enabled date before the gap:
648
-
649
- ```html
650
- <web-daterangepicker
651
- selection-mode="range"
652
- disabled-dates-handling="block">
653
- </web-daterangepicker>
654
- ```
655
-
656
- When dragging from day 1 to day 7 with days 4-5 disabled, the selection will automatically snap to days 1-3.
657
-
658
- **Use case:** Cottage booking where you can't book across existing reservations, or any scenario where gaps in the range are not allowed.
659
-
660
- ### Mode: 'split'
661
-
662
- Returns multiple date ranges separated by disabled dates:
663
-
664
- ```html
665
- <web-daterangepicker
666
- selection-mode="range"
667
- disabled-weekdays="0,6"
668
- disabled-dates-handling="split">
669
- </web-daterangepicker>
670
-
671
- <script>
672
- picker.addEventListener('date-select', (e) => {
673
- console.log('Ranges:', e.detail.dateRanges);
674
- // e.g., [{start: Mon, end: Fri}, {start: Mon, end: Fri}, {start: Mon, end: Fri}]
675
- console.log('Formatted:', e.detail.formattedValue);
676
- // "2025-11-03 - 2025-11-07, 2025-11-10 - 2025-11-14, 2025-11-17 - 2025-11-21"
677
- });
678
- </script>
679
- ```
680
-
681
- **Use case:** Reporting or analytics where you need distinct time periods (e.g., "Generate report for these 3 work weeks").
682
-
683
- ### Mode: 'individual'
684
-
685
- Returns a flat array of individual enabled dates:
686
-
687
- ```html
688
- <web-daterangepicker
689
- selection-mode="range"
690
- disabled-weekdays="0,6"
691
- disabled-dates-handling="individual">
692
- </web-daterangepicker>
693
-
694
- <script>
695
- picker.addEventListener('date-select', (e) => {
696
- console.log('Individual dates:', e.detail.dates);
697
- // [Date(Mon), Date(Tue), Date(Wed), Date(Thu), Date(Fri), Date(Mon), ...]
698
- console.log('Formatted:', e.detail.formattedValue);
699
- // "2025-11-03, 2025-11-04, 2025-11-05, 2025-11-06, 2025-11-07, ..."
700
- });
701
- </script>
702
- ```
703
-
704
- **Use case:** Scheduling or event planning where you need a list of specific dates (e.g., "Schedule training sessions on these dates").
705
-
706
- ### Event Detail Structure by Mode
707
-
708
- | Mode | Properties | Description |
709
- |------|-----------|-------------|
710
- | `allow` | `dateRange`, `enabledDates`, `disabledDates`, `getTotalDays()`, `getEnabledDateCount()` | Full range with helper methods |
711
- | `prevent` | `dateRange`, `dates` | Only enabled dates can be selected |
712
- | `block` | `dateRange`, `dates` | Single continuous range (no disabled dates) |
713
- | `split` | `dateRanges`, `dates` | Multiple ranges split by disabled dates |
714
- | `individual` | `dates` | Flat array of enabled dates |
715
-
716
- ### Visual Highlighting Control
717
-
718
- By default, when you select a range that includes disabled dates, all dates (both enabled and disabled) within the range are visually highlighted. You can change this behavior with the `highlight-disabled-in-range` attribute:
719
-
720
- ```html
721
- <!-- Default: highlights all dates in range, including disabled weekends -->
722
- <web-daterangepicker
723
- selection-mode="range"
724
- disabled-weekdays="0,6"
725
- disabled-dates-handling="split">
726
- </web-daterangepicker>
727
-
728
- <!-- Only highlight enabled dates (Mon-Fri), skip weekends -->
729
- <web-daterangepicker
730
- selection-mode="range"
731
- disabled-weekdays="0,6"
732
- disabled-dates-handling="split"
733
- highlight-disabled-in-range="false">
734
- </web-daterangepicker>
735
- ```
736
-
737
- **When to use `highlight-disabled-in-range="false"`:**
738
- - Selecting working weeks where you only want to see Monday-Friday highlighted
739
- - Visual clarity when disabled dates are not relevant to the selection
740
- - Any scenario where showing gaps in the range is clearer than showing continuous highlighting
741
-
742
- ## Theming
743
-
744
- ### Theme Designer
745
-
746
- The easiest way to customize the appearance of this component is using the **KeenMate Theme Designer** at:
747
-
748
- **[theme-designer.keenmate.dev](https://theme-designer.keenmate.dev)**
749
-
750
- #### How It Works
751
-
752
- 1. **Choose 3 base colors** - background, text, and accent
753
- 2. **Preview changes live** - see your theme applied instantly
754
- 3. **Fine-tune individual variables** - lock specific values while adjusting others
755
- 4. **Export your theme** - copy CSS, JSON, or SCSS to your project
756
-
757
- #### CSS Variable Layers
758
-
759
- KeenMate components support a **two-layer theming architecture**:
760
-
761
- **Standalone Mode (Simple)** - Just override the component-specific variables you need:
762
-
763
- ```css
764
- :root {
765
- --drp-accent-color: #your-brand-color;
766
- --drp-primary-bg: #your-background;
767
- --drp-text-primary: #your-text-color;
768
- }
769
- ```
770
-
771
- **Cascading Mode (Multi-Component)** - When using multiple KeenMate components, you can define a shared base layer:
772
-
773
- ```css
774
- :root {
775
- /* Base layer - single source of truth */
776
- --base-accent-color: #3b82f6;
777
- --base-primary-bg: #ffffff;
778
- --base-text-primary: #111827;
779
-
780
- /* Components reference base layer */
781
- --ms-accent-color: var(--base-accent-color);
782
- --drp-accent-color: var(--base-accent-color);
783
- }
784
- ```
785
-
786
- Change `--base-accent-color` once all components update automatically.
787
-
788
- #### Unified Variable Naming
789
-
790
- All KeenMate components follow a consistent naming convention for **Tier 1 variables** (core theming):
791
-
792
- | Purpose | web-multiselect | web-daterangepicker |
793
- |---------|-----------------|---------------------|
794
- | Brand color | `--ms-accent-color` | `--drp-accent-color` |
795
- | Background | `--ms-primary-bg` | `--drp-primary-bg` |
796
- | Text color | `--ms-text-primary` | `--drp-text-primary` |
797
- | Text on accent | `--ms-text-on-accent` | `--drp-text-on-accent` |
798
- | Border color | `--ms-border-color` | `--drp-border-color` |
799
-
800
- Learn the pattern once, apply it across all components.
801
-
802
- #### Component Variables Manifest
803
-
804
- This package exports a `component-variables.manifest.json` file that documents all supported CSS variables for tooling integration (e.g., Theme Designer, IDE autocomplete):
805
-
806
- ```javascript
807
- import manifest from '@keenmate/web-daterangepicker/component-variables.manifest.json';
808
- // manifest.baseVariables - list of --base-* variables the component responds to
809
- // manifest.componentVariables - list of --drp-* component-specific variables
810
- ```
811
-
812
- ### CSS Custom Properties
813
-
814
- Customize the appearance using CSS custom properties:
815
-
816
- ```css
817
- :root {
818
- /* Base Unit - scale entire component by changing this */
819
- --drp-rem: 10px; /* Default base unit (change to scale everything) */
820
-
821
- /* Colors */
822
- --drp-dropdown-background: #ffffff;
823
- --drp-border-color: #e5e7eb;
824
- --drp-primary-bg: #f3f4f6;
825
- --drp-primary-bg-hover: #e5e7eb;
826
- --drp-accent-color: #3b82f6;
827
- --drp-accent-color-hover: #2563eb;
828
- --drp-text-primary: #111827;
829
- --drp-text-secondary: #6b7280;
830
- --drp-text-on-accent: #ffffff;
831
-
832
- /* Input Field */
833
- --drp-input-background: var(--drp-dropdown-background);
834
- --drp-input-color: var(--drp-text-primary);
835
- --drp-input-border: var(--base-input-border, var(--drp-border));
836
- --drp-input-border-hover: var(--base-input-border-hover, var(--drp-border-width-base) solid var(--drp-accent-color));
837
- --drp-input-border-focus: var(--base-input-border-focus, var(--drp-border-width-base) solid var(--drp-accent-color));
838
- --drp-input-placeholder-color: var(--drp-text-secondary);
839
-
840
- /* Typography (all scale with --drp-rem) */
841
- --drp-font-size-xs: calc(1.2 * var(--drp-rem)); /* 12px */
842
- --drp-font-size-sm: calc(1.4 * var(--drp-rem)); /* 14px */
843
- --drp-font-size-base: calc(1.6 * var(--drp-rem)); /* 16px */
844
- --drp-font-weight-medium: 500;
845
- --drp-font-weight-semibold: 600;
846
-
847
- /* Spacing (all scale with --drp-rem) */
848
- --drp-spacing-xs: calc(0.4 * var(--drp-rem)); /* 4px */
849
- --drp-spacing-sm: calc(0.8 * var(--drp-rem)); /* 8px */
850
- --drp-spacing-md: calc(1.6 * var(--drp-rem)); /* 16px */
851
-
852
- /* Borders */
853
- --drp-border-width-base: 1px;
854
- --drp-border-radius-sm: calc(var(--base-border-radius-sm, 0.4) * var(--drp-rem)); /* 4px - day cells, tooltips */
855
- --drp-border-radius-md: calc(var(--base-border-radius-md, 0.6) * var(--drp-rem)); /* 6px - inputs, buttons */
856
- --drp-border-radius-lg: calc(var(--base-border-radius-lg, 0.8) * var(--drp-rem)); /* 8px - calendar, dropdowns */
857
- --drp-border-radius: var(--drp-border-radius-md); /* default alias */
858
-
859
- /* Shadows */
860
- --drp-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
861
-
862
- /* Transitions */
863
- --drp-transition-fast: 150ms;
864
- --drp-easing-snappy: cubic-bezier(0.4, 0.0, 0.2, 1);
865
- }
866
- ```
867
-
868
- ### Input Size Scale
869
-
870
- The component uses a 10px-based sizing system (`--drp-rem: 10px`) for clean, predictable dimensions:
871
-
872
- | Size | Attribute | Height | Base Variable |
873
- |------|-----------|--------|---------------|
874
- | XS | `input-size="xs"` | 31px | `--base-input-size-xs-height` |
875
- | SM | `input-size="sm"` | 33px | `--base-input-size-sm-height` |
876
- | MD | `input-size="md"` | 35px (default) | `--base-input-size-md-height` |
877
- | LG | `input-size="lg"` | 38px | `--base-input-size-lg-height` |
878
- | XL | `input-size="xl"` | 41px | `--base-input-size-xl-height` |
879
-
880
- **Theme Designer Integration**: Input heights reference `--base-input-size-*-height` variables from the [Theme Designer](https://theme-designer.keenmate.dev). This ensures consistent input heights across all KeenMate components (web-multiselect, web-daterangepicker).
881
-
882
- For complete size variable reference (font sizes, padding, spacing), see [SIZES.md](SIZES.md).
883
-
884
- #### Customizing Input Heights
885
-
886
- Four ways to customize input dimensions:
887
-
888
- ```css
889
- /* Option 1: Via Theme Designer base variables (recommended for multi-component consistency) */
890
- :root {
891
- --base-input-size-md-height: 4.2; /* All components: 42px at 10px rem */
892
- }
893
-
894
- /* Option 2: Direct px override */
895
- web-daterangepicker {
896
- --drp-input-size-md-height: 42px;
897
- }
898
-
899
- /* Option 3: Scale all sizes via --drp-rem */
900
- web-daterangepicker {
901
- --drp-rem: 12px; /* MD = 3.5 × 12 = 42px */
902
- }
903
-
904
- /* Option 4: Override specific size with calc */
905
- web-daterangepicker {
906
- --drp-input-size-md-height: calc(4.2 * var(--drp-rem));
907
- }
908
- ```
909
-
910
- ### Calendar Scaling
911
-
912
- Scale the entire calendar by setting `--drp-rem` directly on the `<web-daterangepicker>` element:
913
-
914
- ```css
915
- /* Compact size (80%) */
916
- web-daterangepicker.compact {
917
- --drp-rem: 8px;
918
- }
919
-
920
- /* Large size (150%) */
921
- web-daterangepicker.large {
922
- --drp-rem: 15px;
923
- }
924
-
925
- /* Or use inline style */
926
- <web-daterangepicker style="--drp-rem: 12px;"></web-daterangepicker>
927
- ```
928
-
929
- **Important:** Due to Shadow DOM, CSS variables must be set on the `<web-daterangepicker>` element itself (via class or inline style), not on a wrapper div.
930
-
931
- For fine-grained control, override individual variables:
932
-
933
- ```css
934
- web-daterangepicker.custom {
935
- --drp-rem: 12px;
936
- --drp-spacing-xs: 2px; /* Tighter gaps */
937
- --drp-font-size-base: 18px; /* Larger text */
938
- }
939
- ```
940
-
941
- See [examples-sizes.html](examples-sizes.html) for interactive demos.
942
-
943
- ## Development
944
-
945
- ```bash
946
- # Install dependencies
947
- npm install
948
-
949
- # Start dev server
950
- npm run dev
951
-
952
- # Build for production
953
- npm run build
954
-
955
- # Preview production build
956
- npm run preview
957
- ```
958
-
959
- ## Browser Support
960
-
961
- - Modern browsers with Web Components and CSS `color-mix()` support
962
- - Chrome/Edge 111+
963
- - Firefox 113+
964
- - Safari 16.2+
965
-
966
- For older browser support, use the compiled `dist/style.css` which is processed by Vite.
967
-
968
- ## HTML Injection (XSS) Notice
969
-
970
- The following callbacks and methods allow **raw HTML injection** and are intentionally **NOT XSS-safe**. This gives developers full control over rendering but requires sanitizing untrusted data:
971
-
972
- | Callback/Method | Output Used In | Risk Level |
973
- |-----------------|---------------|------------|
974
- | `showMessage(html)` | Message area (innerHTML) | HTML injection |
975
- | `renderDayCallback` | Day cells (innerHTML) | HTML injection |
976
- | `renderDayContentCallback` | Day cells (innerHTML) | HTML injection |
977
- | `getDateMetadataCallback` (badgeText, dayTooltip) | Badges/tooltips (innerHTML) | HTML injection |
978
- | `formatSummaryCallback` | Summary display (innerHTML) | HTML injection |
979
- | `getMonthHeaderCallback` | Month headers (innerHTML) | HTML injection |
980
- | `getUnifiedHeaderCallback` | Unified header (innerHTML) | HTML injection |
981
- | `customStylesCallback` | Style tag (textContent) | CSS injection |
982
- | `actionButtons[].label` | Button labels (innerHTML) | HTML injection |
983
-
984
- **Safe callbacks** (output is escaped or used as data):
985
- - `beforeDateSelectCallback`, `beforeMonthChangedCallback` (return action objects)
986
- - `onSelect`, `onChange` (event handlers)
987
- - `getDateMetadataCallback` (isDisabled, dayClass, badgeClass - CSS class names only)
988
-
989
- **If displaying user-generated content**, sanitize it before passing to these callbacks or methods.
990
-
991
- ## Changelog
992
-
993
- See [CHANGELOG.md](https://github.com/keenmate/web-daterangepicker/blob/main/CHANGELOG.md) for version history and migration guides.
994
-
995
- ## License
996
-
997
- MIT
998
-
999
- ## Credits
1000
-
1001
- Extracted from the [Pure Admin](https://github.com/keenmate/pure-admin) design system.
1
+ # Date Range Picker Web Component
2
+
3
+ A lightweight, accessible date picker web component with excellent keyboard navigation and range selection support.
4
+
5
+ > **⚠️ Security Notice:** This component intentionally allows raw HTML in rendering callbacks and message content to give developers full control over content display. If you display user-generated content, you must sanitize it yourself. See [HTML Injection (XSS) Notice](#html-injection-xss-notice) for the complete list of affected callbacks and methods.
6
+
7
+ ## What's New in v1.14.0-rc01
8
+
9
+ - **Three new time-display UIs — clock, wheel, compact** — `time-display` now accepts four values: `rolls` (default, unchanged), `clock` (Material-style two-step face for hours then minutes, with h12 / h24 dual-ring support), `wheel` (iOS UIPickerView snap-scrolling columns with center selection band), and `compact` (iOS 14+ tappable pills with `contentEditable` typing and Arrow-key increments). All three work in `picker-mode="time"` and `picker-mode="datetime"`; `rolls` stays the default so existing time pickers are unchanged. See `examples-time-picker.html` for the demo gallery.
10
+ - **OS-aware light/dark defaults via `light-dark()`** set `color-scheme: dark` on your page (`:root`, `body`, etc.) and the picker picks readable dark text/background colors automatically. No more enumerating ~15 `--base-*` overrides just to get usable defaults on a dark theme.
11
+ - **Drift-detection warning for calendar positioning** — if an exotic ancestor CSS property (e.g. `contain: paint`, or `container-type` in certain shadow-DOM layouts) makes the calendar land somewhere other than where the library told the browser to put it, a `console.warn` fires once with the likely culprit element and an actionable fix suggestion.
12
+ - **Calendar no longer stranded to the side when an ancestor uses `container-type`** Floating UI was walking up to a `container-type: inline-size` ancestor (notably pure-admin's `.pa-layout__main`), but the browser wouldn't actually anchor the fixed calendar there. The library now uses a custom `getOffsetParent` that only walks up properties browsers reliably honor for fixed positioning. Same fix applied to day-cell tooltips.
13
+ - **`--base-*` taxonomy aligned with KeenMate cross-component naming** (theming change — see CHANGELOG migration table): `--drp-primary-bg` now reads `--base-hover-bg` (was `--base-main-bg`); `--drp-primary-bg-hover` now reads `--base-active-bg` (was `--base-hover-bg`). `--base-dropdown-bg` and `--base-tooltip-bg` continue to work; new chain fallbacks to `--base-elevated-bg` / `--base-inverse-bg`. Mirrors web-multiselect v1.11.0 so multi-component theming stays coherent.
14
+ - **Day hover stays visible on dark themes** — `--drp-primary-bg` now mixes 8% of the text color into the main background by default, so the hover is always a visible step toward the text. No more invisible hover when the consumer forgets to override `--base-hover-bg`.
15
+ - **Message colors (`--drp-message-*`) got dark-mode companions** error/warning/info/success palettes now resolve via `light-dark()` instead of hardcoded bright pastels, so feedback messages stay readable on dark surfaces without overrides.
16
+ - **New dark-mode e2e suite** — 4 specs verifying WCAG-AA day-cell contrast on a dark page across fully-themed, minimal-override, and pure OS-inheritance configurations.
17
+ - **Framework-class + per-instance dark/light overrides** — set `data-theme="dark"`, `data-bs-theme="dark"` (Bootstrap 5.3+), or `class="dark"` (Tailwind) on any ancestor and the picker switches to its dark palette. Set it on the `<web-daterangepicker>` element itself to theme one instance independently of the page. Symmetric `light` selectors restore the light palette so a single widget can be forced light on a dark page.
18
+ - **CSS cascade layers** `main.css` declares `@layer variables, component, overrides;`. Consumer-side: any unlayered `web-daterangepicker { … }` rule beats every internal rule without `!important`, and any `:root { --base-X: … }` declaration beats the variables layer.
19
+ - **CSS file naming refresh** — partials under `src/css/` no longer carry the SASS-style underscore prefix (`_variables.css` `variables.css`, etc.). The two package-exports paths (`./css/variables`, `./css/base`) keep their public names. Consumers importing internal files directly via `./src/css/_*.css` must update their import paths.
20
+ - **Canonical Tier-2 file set.** New stylesheets `controls.css`, `floating.css`, `states.css`, `animations.css` matching the cross-component guideline. The old `tooltips.css` and `modifiers.css` partials were merged into the new files and removed.
21
+ - **BEM short-prefix class names.** Internal classes were migrated from the long `.drp-date-picker__*` form to the canonical short `.drp__*` form (`.drp__day`, `.drp__month`, `.drp__rolling-item`, etc.). The root container `.drp-date-picker` is now `.drp__picker`; `.drp-input` and the legacy `.drp-date-picker-input` aliases are unified under `.drp__input`. Public variable-only theming (`web-daterangepicker { --drp-X: ... }`) is unaffected — only consumers using `customStylesCallback` to inject CSS into the shadow root need to update their selectors. See CHANGELOG v1.16.0 for the full migration table.
22
+
23
+ ## What's New in v1.14.0
24
+
25
+ - **Time picker and datetime mode** — new `picker-mode` attribute with values `date` (default, unchanged), `time` (rolls-only popover for hours/minutes), and `datetime` (calendar grid + time rolls side-by-side in one popover). Orthogonal to the existing `selection-mode` so date-mode behavior is untouched. v1 supports time/datetime in `single` mode only; `range`/`multiple` silently falls back with a console warning (datetime range lands in v1.15).
26
+ - **Rolling-list time UI reuses the year/month picker pattern** — two-to-four roll columns (hours, minutes, optional seconds, optional AM/PM) sharing the existing `.drp-date-picker__rolling-list` + `.drp-date-picker__rolling-item` classes, so all existing `--drp-rolling-*` theming hooks apply. Click an item to commit; the highlighted item snaps to the current selection.
27
+ - **Time config attributes** — `time-format-mask` (tokens `HH`/`H`/`hh`/`h`/`mm`/`m`/`ss`/`s`/`a`, default `HH:mm`), `display-time-format-mask` (localized placeholder parallel to `display-format-mask`), `time-step` (minute/second increment in the rolls, default `1`), `hour-cycle` (`h12`/`h24`, auto-derived from the mask), `show-seconds` (auto-derived from the `s` token), `show-now-button` (defaults true in time/datetime, parallel to `show-today-button`).
28
+ - **`autoClose` defaults to `'apply'` in time/datetime modes** so each roll-click updates the pending selection rather than slamming the popover shut between hour and minute. User-supplied `auto-close` still wins.
29
+ - **`normalizeDate()` gained a `preserveTime` flag** (default `false`, so every existing caller is unchanged). `initialDate` parsing flips it on in time/datetime, so `initial-date="2026-05-23T14:30"` survives both the parse and ISO string handling. Disabled-dates / range-validation helpers stay midnight by design.
30
+ - **New locale strings** — `time`, `now`, `am`, `pm` added to `LocaleStrings` and hardcoded for the four bundled locales (en/de/fr/es). `customStrings` override still works.
31
+ - **CSS surface** — new `_time-picker.css` partial with `--drp-time-picker-*` variables. New BEM classes `.drp-date-picker__main`, `.drp-date-picker__time-picker`, `.drp-date-picker__time-rolls`, `.drp-date-picker__time-roll`, `.drp-date-picker__time-separator`, `.drp-date-picker__time-label`, plus root modifiers `.drp-date-picker--time` and `.drp-date-picker--datetime`. Container query collapses the date|time row to vertical at narrow widths (mobile / modal).
32
+ - **v1 scope limitations** (documented, not bugs): time/datetime + range/multiple falls back; datetime + `month-layout="grid"` forces horizontal (warning); input mask only parses dates (committed H/M/S survive reopens because the picker's selection is authoritative); per-hour disabling not supported (`disabledDates` is whole-day only); keyboard navigation short-circuited in time mode for v1; `min-time` / `max-time` deferred.
33
+
34
+ ## Features
35
+
36
+ - 🎯 **Input Masking** - Auto-format dates as you type with separator insertion
37
+ - ⌨️ **Keyboard Navigation** - Full keyboard support (arrows, Enter, Esc, PageUp/Down, Home/End)
38
+ - 📅 **Rolling Selector** - Innovative scrollable year/month picker
39
+ - 📊 **Multi-Month Display** - Show 1-3+ months side by side with independent navigation
40
+ - 🎨 **Themeable** - All styles use CSS custom properties (`--drp-*`)
41
+ - 🖱️ **Drag-to-Adjust** - Drag range endpoints to adjust selection (range mode)
42
+ - 👀 **Live Hover Preview** - See the would-be range painted live as you move toward an end date (range mode, mode-aware per `disabled-dates-handling`)
43
+ - 🌐 **Multiple Formats** - YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY, etc.
44
+ - 🌍 **Locale-Aware** - Auto-detect week start day from user's locale
45
+ - 🚫 **Date Restrictions** - Min/max dates, disabled days/dates, custom disable logic
46
+ - 🎉 **Special Dates** - Highlight holidays, events with custom labels and styling
47
+ - ✨ **Modern** - Web Component with Shadow DOM, TypeScript, bundled with Vite
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install @keenmate/web-daterangepicker
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Basic HTML
58
+
59
+ ```html
60
+ <!-- Single date picker -->
61
+ <web-daterangepicker
62
+ selection-mode="single"
63
+ date-format-mask="YYYY-MM-DD"
64
+ placeholder="Select date"
65
+ ></web-daterangepicker>
66
+
67
+ <!-- Date range picker -->
68
+ <web-daterangepicker
69
+ selection-mode="range"
70
+ date-format-mask="YYYY-MM-DD"
71
+ visible-months-count="2"
72
+ placeholder="Select date range"
73
+ ></web-daterangepicker>
74
+ ```
75
+
76
+ ### With JavaScript/TypeScript
77
+
78
+ ```typescript
79
+ // Import the component (includes styles)
80
+ import '@keenmate/web-daterangepicker';
81
+
82
+ // Or import styles separately if needed
83
+ import '@keenmate/web-daterangepicker/style.css';
84
+
85
+ const picker = document.querySelector('web-daterangepicker');
86
+
87
+ // Listen for date selection
88
+ picker.addEventListener('date-select', (e) => {
89
+ console.log('Selected:', e.detail.formattedValue);
90
+ console.log('Date object:', e.detail.date);
91
+ console.log('Range:', e.detail.dateRange);
92
+ });
93
+
94
+ // Programmatic API
95
+ picker.show(); // Show calendar
96
+ picker.hide(); // Hide calendar
97
+ picker.toggle(); // Toggle calendar
98
+ picker.clearSelection(); // Clear selection
99
+ picker.getInputValue(); // Get current value
100
+ picker.setInputValue('2025-11-15'); // Set value
101
+ ```
102
+
103
+ ### JavaScript Instantiation (Direct Class Usage)
104
+
105
+ If you prefer to use the `DateRangePicker` class directly instead of the web component, you have full programmatic control:
106
+
107
+ ```typescript
108
+ import { DateRangePicker } from '@keenmate/web-daterangepicker';
109
+ // IMPORTANT: Import CSS separately (web component auto-injects, but the class doesn't)
110
+ import '@keenmate/web-daterangepicker/dist/style.css';
111
+
112
+ const inputElement = document.getElementById('my-input');
113
+
114
+ const picker = new DateRangePicker(inputElement, {
115
+ selectionMode: 'range',
116
+ visibleMonthsCount: 2,
117
+ dateFormatMask: 'YYYY-MM-DD',
118
+ onSelect: (detail) => {
119
+ // Range mode: detail is { start: Date, end: Date }
120
+ console.log('Range:', detail.start, 'to', detail.end);
121
+ }
122
+ });
123
+ ```
124
+
125
+ **⚠️ Critical: CSS Requirements**
126
+
127
+ Unlike the web component, the `DateRangePicker` class does **NOT** automatically inject styles. You **MUST** import CSS separately or you'll see an unstyled calendar. Choose one method:
128
+
129
+ **Option 1: Import CSS in JavaScript (Recommended)**
130
+ ```typescript
131
+ import { DateRangePicker } from '@keenmate/web-daterangepicker';
132
+ import '@keenmate/web-daterangepicker/dist/style.css';
133
+ ```
134
+
135
+ **Option 2: Link CSS in HTML**
136
+ ```html
137
+ <link rel="stylesheet" href="./node_modules/@keenmate/web-daterangepicker/dist/style.css">
138
+ ```
139
+
140
+ **Option 3: Programmatic Injection**
141
+ ```typescript
142
+ import { DateRangePicker } from '@keenmate/web-daterangepicker';
143
+
144
+ // Inject styles once before creating pickers
145
+ DateRangePicker.injectGlobalStyles();
146
+
147
+ const picker = new DateRangePicker(inputElement, options);
148
+ ```
149
+
150
+ **Why?** The web component uses Shadow DOM and injects styles into its isolated scope automatically. The `DateRangePicker` class creates calendar elements in the regular DOM, so it expects global CSS to be loaded separately.
151
+
152
+ See the [JavaScript Instantiation Examples](examples-javascript-instantiation.html) for complete code samples and configuration options.
153
+
154
+ ## Attributes
155
+
156
+ > **Note:** HTML attributes use kebab-case (e.g., `selection-mode`), while JavaScript options use camelCase (e.g., `selectionMode`).
157
+
158
+ | Attribute | Type | Default | Description |
159
+ |-----------|------|---------|-------------|
160
+ | `selection-mode` | `'single' \| 'range'` | `'single'` | Single date or date range selection |
161
+ | `date-format-mask` | `string` | `'YYYY-MM-DD'` | Date format (YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY, etc.) |
162
+ | `visible-months-count` | `number` | `1` (single), `2` (range) | Number of months to display |
163
+ | `calendar-open-trigger` | `'focus' \| 'typing' \| 'manual'` | `'focus'` | How to open calendar (focus = on input focus, typing = when user types, manual = programmatic only) |
164
+ | `value` | `string` | - | Current value |
165
+ | `placeholder` | `string` | - | Input placeholder text |
166
+ | `disabled` | `boolean` | `false` | Disable the picker |
167
+ | `week-start-day` | `'auto' \| 0-6` | `'auto'` | First day of week (0=Sunday, 1=Monday, etc. 'auto'=detect from locale) |
168
+ | `min-date` | `string` | - | Minimum selectable date (YYYY-MM-DD) |
169
+ | `max-date` | `string` | - | Maximum selectable date (YYYY-MM-DD) |
170
+ | `disabled-weekdays` | `string` | - | Comma-separated day numbers to disable (e.g., "0,6" for weekends) |
171
+ | `disabled-dates` | `string` | - | Comma-separated ISO dates to disable (e.g., `"2026-06-13, 2026-06-14, 2026-12-25"`). The `disabledDates` property still wins if both paths are set. |
172
+ | `disabled-dates-handling` | `'allow' \| 'prevent' \| 'block' \| 'split' \| 'individual'` | `'allow'` | How to handle range selections over disabled dates (see [Range Selection Modes](#range-selection-modes)) |
173
+ | `display-format-mask` | `string` | Same as `date-format-mask` | Localized format hint shown to users (e.g., `'dd/mm/aaaa'` Spanish, `'tt.mm.jjjj'` German). Used as the input placeholder when no explicit `placeholder` is set. Validation still uses `date-format-mask`. |
174
+ | `highlight-disabled-in-range` | `boolean` | `true` | Whether to visually highlight disabled dates within a selected range. Set to `false` to only highlight enabled dates. |
175
+ | `auto-close` | `'never' \| 'selection' \| 'apply'` | `'selection'` | When to close calendar (selection = after picking, apply = after Apply button, never = manual) |
176
+ | `positioning-mode` | `'inline' \| 'floating' \| 'modal'` | `'floating'` | Calendar positioning (inline = embedded, floating = popup anchored to input, modal = centered overlay with backdrop) |
177
+ | `mobile-modal-breakpoint` | CSS length (e.g., `"640px"`, `"40em"`) | | When configured mode is `floating`, auto-switch to `modal` below this viewport width. |
178
+ | `mobile-modal-min-height` | CSS length | — | Same idea but for viewport height — modal engages when viewport height is below the threshold. ORs with `mobile-modal-breakpoint`. |
179
+ | `show-summary` | `boolean` | `true` | Show range-mode days/nights summary block. Set to `false` to omit entirely (no empty-div jump). |
180
+ | `input-size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Input field size (floating/modal modes only) |
181
+ | `enable-transitions` | `boolean` | `false` | Enable CSS transitions/animations |
182
+ | `date-member` | `string` | `'date'` | Field name on `specialDates` objects holding the date. Lets you reuse existing data shapes without renaming keys. |
183
+ | `badge-text-member` | `string` | `'badgeText'` | Field name on `specialDates` objects holding the badge label. |
184
+ | `badge-class-member` | `string` | `'badgeClass'` | Field name on `specialDates` objects holding the badge's extra CSS class. |
185
+ | `day-class-member` | `string` | `'dayClass'` | Field name on `specialDates` objects holding the day cell's extra CSS class. |
186
+ | `badge-tooltip-member` | `string` | `'badgeTooltip'` | Field name on `specialDates` objects holding the badge tooltip text. |
187
+ | `day-tooltip-member` | `string` | `'dayTooltip'` | Field name on `specialDates` objects holding the day tooltip text. |
188
+ | `is-disabled-member` | `string` | `'isDisabled'` | Field name on `specialDates` objects holding the disabled flag. |
189
+
190
+ ## Properties
191
+
192
+ ```typescript
193
+ // Get/set properties (use camelCase in JavaScript)
194
+ picker.selectionMode = 'range';
195
+ picker.dateFormatMask = 'DD.MM.YYYY';
196
+ picker.value = '2025-11-15';
197
+ picker.disabled = true;
198
+
199
+ // Complex data (arrays / objects / callbacks) — set via property, not attribute
200
+ picker.disabledDates = ['2026-06-13', '2026-06-14'];
201
+ picker.specialDates = [{ date: '2026-12-25', badgeText: '🎄', badgeTooltip: 'Christmas' }];
202
+
203
+ // Localization overrides
204
+ picker.customStrings = { today: 'Jump', clear: 'Wipe' };
205
+ picker.monthNames = ['01','02','03','04','05','06','07','08','09','10','11','12'];
206
+ ```
207
+
208
+ > Property setters work even before the element is upgraded — assignments made before `customElements.define()` runs are routed through the accessors during `connectedCallback`, so you don't need `customElements.whenDefined('web-daterangepicker')` guards.
209
+
210
+ ## Methods
211
+
212
+ | Method | Description |
213
+ |--------|-------------|
214
+ | `show()` | Show the calendar (floating mode only) |
215
+ | `hide()` | Hide the calendar |
216
+ | `toggle()` | Toggle calendar visibility |
217
+ | `clearSelection()` | Clear the current selection |
218
+ | `getInputValue()` | Get the current value as a string |
219
+ | `setInputValue(value: string)` | Set the value |
220
+ | `showMessage(html: string)` | Display a message in the calendar with custom HTML content |
221
+ | `hideMessage()` | Hide the currently displayed message |
222
+
223
+ ## Events
224
+
225
+ | Event | Detail | Description |
226
+ |-------|--------|-------------|
227
+ | `date-select` | `{ date?, dateRange?, formattedValue }` | Fired when a date is selected |
228
+ | `change` | `{ date?, dateRange?, formattedValue }` | Fired when selection changes |
229
+ | `custom-action` | `{ [key: string]: string }` | Fired when a button with `data-action="custom"` is clicked. Detail contains all `data-*` attributes as camelCase keys. |
230
+
231
+ > There are no separate `apply` or `cancel` events. The Apply button commits the pending selection and dispatches `change`. Pressing Escape with an uncommitted selection silently restores the input value and fires nothing.
232
+
233
+ ## Keyboard Shortcuts
234
+
235
+ - **↑ ↓** - Navigate up/down by week
236
+ - **← →** - Navigate left/right by day
237
+ - **Ctrl+← / Ctrl+→** - Previous / next month (maintains day position)
238
+ - **PageUp / PageDown** - Previous / next month (maintains day position)
239
+ - **Home** - First day of current month (repeat to cycle backwards through months)
240
+ - **End** - Last day of current month (repeat to cycle forwards through months)
241
+ - **Ctrl+Home** - January 1st of current year (repeat for previous year)
242
+ - **Ctrl+End** - December 31st of current year (repeat for next year)
243
+ - **Enter** - Select focused day
244
+ - **Escape** - Close calendar
245
+ - **Tab / Shift+Tab** - Switch between month columns (multi-month mode)
246
+ - **T** - Jump to today
247
+
248
+ ## ⚠️ Working with Dates Across Timezones
249
+
250
+ This is a **calendar-date picker** — it represents days, not moments in time. Skipping this section will eventually cost you a one-day-shift bug, so please read it.
251
+
252
+ ### How the picker represents dates
253
+
254
+ When the user clicks April 30, the picker stores `new Date(year, month - 1, day)` — that's **local midnight** on the picked day. The same is true for `selectedDate`, `selectedStartDate`, `selectedEndDate`, and the Date passed to every callback. There's no UTC anywhere in the picker's internal lookup paths.
255
+
256
+ ### The trap: `Date.prototype.toISOString()`
257
+
258
+ `toISOString()` returns the date in **UTC**. For any user not in UTC, that string represents a different *calendar* day than the one they see:
259
+
260
+ ```js
261
+ // User in Moscow (UTC+3) picks April 30.
262
+ // selectedDate is `new Date(2026, 3, 30)` = April 30 00:00 MSK = April 29 21:00 UTC.
263
+
264
+ selectedDate.toISOString().split('T')[0] // → "2026-04-29" wrong day
265
+ selectedDate.getDate() //30 ✅ what the user clicked
266
+ ```
267
+
268
+ This is what broke the example demos in earlier versions: lookup keys built from `dayOffset()` (local) didn't match keys derived from `toISOString()` (UTC). Badges rendered one day late, tooltips on the right day. Same bug strikes any user who tries `date.toISOString()` to key into a `Map<string, DateInfo>`.
269
+
270
+ ### The rule
271
+
272
+ **Format dates from local components, never from `toISOString()`:**
273
+
274
+ ```js
275
+ const toLocalISO = (d) =>
276
+ `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
277
+
278
+ picker.getDateMetadataCallback = (date) => {
279
+ const dateStr = toLocalISO(date); // ✅ matches what the user sees
280
+ // …lookup, return DateInfo…
281
+ };
282
+ ```
283
+
284
+ ### Sending dates to a server / between users
285
+
286
+ `Date` objects can't survive transport — they get serialized. The serialization format determines whether the date stays on the right calendar day across timezones:
287
+
288
+ | What you send | Russian user picks Apr 30 → LA user receives | Verdict |
289
+ |---|---|---|
290
+ | `event.detail.formattedValue` (`"2026-04-30"`) | `"2026-04-30"` → set on LA picker → April 30 | ✅ |
291
+ | `JSON.stringify(detail)` (Date ISO string) | `"2026-04-29T21:00:00.000Z"` April 29 in LA | ❌ |
292
+ | `selectedDate.toISOString()` | `"2026-04-29T21:00:00.000Z"` April 29 in LA | |
293
+ | `toLocalISO(selectedDate)` (`"2026-04-30"`) | `"2026-04-30"` | ✅ |
294
+
295
+ **Recommendation:** transmit calendar dates as `YYYY-MM-DD` **strings**, never as ISO timestamps or raw `Date`/JSON-serialized payloads.
296
+
297
+ ### Receiving a date string
298
+
299
+ `new Date("2026-04-30")` parses as **UTC midnight** — surprising for non-UTC users (in LA it'd be April 29). Build local Dates from the string parts:
300
+
301
+ ```js
302
+ // ❌ Don't do this
303
+ const d = new Date("2026-04-30"); // UTC midnight, surprising in non-UTC
304
+
305
+ // ✅ Do this
306
+ const [y, m, day] = "2026-04-30".split("-").map(Number);
307
+ const localDate = new Date(y, m - 1, day); // April 30 local — what the user picked
308
+
309
+ // Or, if you just need to set the picker:
310
+ picker.setInputValue("2026-04-30"); // picker handles parsing internally
311
+ picker.value = "2026-04-30"; // attribute setter
312
+ ```
313
+
314
+ ### Quick checklist
315
+
316
+ - [ ] Picker callbacks: format Dates with `toLocalISO()`, not `toISOString()`
317
+ - [ ] Outgoing dates (server, URL, localStorage): send `YYYY-MM-DD` strings
318
+ - [ ] Incoming dates: prefer `picker.setInputValue("YYYY-MM-DD")` over manual `new Date(...)` parsing
319
+ - [ ] Map/dictionary keys for date lookups: build keys with `toLocalISO()` and lookup the same way
320
+
321
+ ## Advanced Features
322
+
323
+ ### Week Start Day
324
+
325
+ Control which day the week starts on (auto-detected by default from user's locale):
326
+
327
+ ```html
328
+ <!-- Auto-detect from locale (default) -->
329
+ <web-daterangepicker week-start-day="auto"></web-daterangepicker>
330
+
331
+ <!-- Force Sunday start -->
332
+ <web-daterangepicker week-start-day="0"></web-daterangepicker>
333
+
334
+ <!-- Force Monday start (common in Europe) -->
335
+ <web-daterangepicker week-start-day="1"></web-daterangepicker>
336
+ ```
337
+
338
+ ### Disabled Dates & Date Restrictions
339
+
340
+ #### Simple Restrictions (Attributes)
341
+
342
+ ```html
343
+ <!-- Disable weekends -->
344
+ <web-daterangepicker disabled-weekdays="0,6"></web-daterangepicker>
345
+
346
+ <!-- Date range restriction -->
347
+ <web-daterangepicker
348
+ min-date="2025-01-01"
349
+ max-date="2025-12-31">
350
+ </web-daterangepicker>
351
+ ```
352
+
353
+ #### Complex Restrictions (JavaScript)
354
+
355
+ ```javascript
356
+ const picker = document.querySelector('web-daterangepicker');
357
+
358
+ // Disable specific dates (e.g., public holidays)
359
+ picker.disabledDates = [
360
+ '2025-12-25', // Christmas
361
+ '2025-01-01', // New Year
362
+ new Date(2025, 6, 4) // July 4th
363
+ ];
364
+
365
+ // Custom disable logic (e.g., cottage booking)
366
+ picker.getDateMetadataCallback = (date) => {
367
+ // Disable all dates that overlap with existing bookings
368
+ const isBooked = bookedRanges.some(range =>
369
+ date >= range.start && date <= range.end
370
+ );
371
+ return isBooked ? { isDisabled: true } : null;
372
+ };
373
+ ```
374
+
375
+ ### Special Dates (Holidays, Events)
376
+
377
+ Add visual indicators and labels to specific dates:
378
+
379
+ ```javascript
380
+ const picker = document.querySelector('web-daterangepicker');
381
+
382
+ picker.specialDates = [
383
+ {
384
+ date: '2025-12-25',
385
+ dayClass: 'holiday', // CSS class for the day cell
386
+ badgeText: '🎄', // Badge overlay (emoji or short text)
387
+ dayTooltip: 'Christmas Day'
388
+ },
389
+ {
390
+ date: '2025-07-04',
391
+ dayClass: 'holiday',
392
+ badgeText: '🎆',
393
+ dayTooltip: 'Independence Day'
394
+ },
395
+ {
396
+ date: '2025-02-14',
397
+ dayClass: 'event',
398
+ badgeText: '❤️',
399
+ dayTooltip: 'Valentine\'s Day'
400
+ }
401
+ ];
402
+ ```
403
+
404
+ ### Advanced Styling & Info
405
+
406
+ For complete control, use the `getDateMetadataCallback`:
407
+
408
+ ```javascript
409
+ // Local-date formatter — see "Working with Dates Across Timezones" above.
410
+ // Don't use date.toISOString() here; it shifts by one day in non-UTC timezones.
411
+ const toLocalISO = (d) =>
412
+ `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
413
+
414
+ picker.getDateMetadataCallback = (date) => {
415
+ const dateStr = toLocalISO(date);
416
+
417
+ // Check if it's a peak season date
418
+ if (isPeakSeason(date)) {
419
+ return {
420
+ dayClass: 'peak-season',
421
+ badgeText: '$$$',
422
+ dayTooltip: 'Peak season pricing'
423
+ };
424
+ }
425
+
426
+ // Check if it's a special offer date
427
+ if (specialOffers[dateStr]) {
428
+ return {
429
+ dayClass: 'special-offer',
430
+ badgeText: '%',
431
+ dayTooltip: `${specialOffers[dateStr]}% off!`
432
+ };
433
+ }
434
+
435
+ return null;
436
+ };
437
+ ```
438
+
439
+ ### CSS Styling for Special Dates
440
+
441
+ Since the component uses Shadow DOM, inject custom styles via the `customStylesCallback`:
442
+
443
+ ```javascript
444
+ picker.customStylesCallback = () => `
445
+ /* Holiday styling */
446
+ .drp-date-picker__day.holiday {
447
+ background-color: rgba(239, 68, 68, 0.1);
448
+ }
449
+
450
+ /* Event styling */
451
+ .drp-date-picker__day.event {
452
+ background-color: rgba(16, 185, 129, 0.1);
453
+ }
454
+
455
+ /* Custom class example */
456
+ .drp-date-picker__day.peak-season {
457
+ background-color: rgba(251, 191, 36, 0.15);
458
+ font-weight: 600;
459
+ }
460
+ `;
461
+ ```
462
+
463
+ ### Date Selection Validation (beforeDateSelectCallback)
464
+
465
+ Validate or modify date selections before they're applied. Supports async validation (e.g., API calls):
466
+
467
+ ```javascript
468
+ const picker = document.querySelector('web-daterangepicker');
469
+
470
+ picker.beforeDateSelectCallback = async (selection) => {
471
+ // selection is Date (single mode) or { start: Date, end: Date } (range mode)
472
+
473
+ // Example: Check availability via API.
474
+ // Send YYYY-MM-DD strings, not toISOString() the picker is a calendar-date
475
+ // picker, and ISO timestamps shift by ±1 day across timezones.
476
+ const response = await fetch('/api/check-availability', {
477
+ method: 'POST',
478
+ body: JSON.stringify({
479
+ start: toLocalISO(selection.start),
480
+ end: toLocalISO(selection.end)
481
+ })
482
+ });
483
+ const { available, message } = await response.json();
484
+
485
+ if (!available) {
486
+ return {
487
+ action: 'restore',
488
+ message: message,
489
+ showInvalidRange: true // Keep selection visible with error styling
490
+ };
491
+ }
492
+
493
+ return { action: 'accept' };
494
+ };
495
+ ```
496
+
497
+ **Return object options:**
498
+
499
+ | Property | Type | Description |
500
+ |----------|------|-------------|
501
+ | `action` | `'accept' \| 'adjust' \| 'restore' \| 'clear'` | **Required.** What to do with the selection |
502
+ | `message` | `string` | Optional message to display in the calendar |
503
+ | `showInvalidRange` | `boolean` | When `true` with `action: 'restore'`, keeps the invalid selection visible with red error styling |
504
+ | `adjustedDate` | `Date` | For `action: 'adjust'` in single mode - the corrected date |
505
+ | `adjustedStartDate` | `Date` | For `action: 'adjust'` in range mode - the corrected start date |
506
+ | `adjustedEndDate` | `Date` | For `action: 'adjust'` in range mode - the corrected end date |
507
+
508
+ **Action behaviors:**
509
+
510
+ - **`accept`**: Apply the selection as-is. Hides any existing message.
511
+ - **`adjust`**: Apply corrected dates instead (use with `adjustedDate` or `adjustedStartDate`/`adjustedEndDate`)
512
+ - **`restore`**: Revert to previous selection. Use `showInvalidRange: true` to show what was attempted.
513
+ - **`clear`**: Clear the selection entirely.
514
+
515
+ **Example: Minimum nights validation with error display:**
516
+
517
+ ```javascript
518
+ picker.beforeDateSelectCallback = (range) => {
519
+ const nights = Math.floor((range.end - range.start) / (1000 * 60 * 60 * 24));
520
+
521
+ if (nights < 2) {
522
+ return {
523
+ action: 'restore',
524
+ message: 'Minimum 2 nights required',
525
+ showInvalidRange: true // Shows attempted range with red styling
526
+ };
527
+ }
528
+
529
+ if (nights > 14) {
530
+ return {
531
+ action: 'restore',
532
+ message: 'Maximum 14 nights allowed',
533
+ showInvalidRange: true
534
+ };
535
+ }
536
+
537
+ return { action: 'accept' };
538
+ };
539
+ ```
540
+
541
+ ### Bulk Metadata Loading (beforeMonthChangedCallback)
542
+
543
+ Load metadata for all visible dates in a single API call when the user navigates months. Much more efficient than `getDateMetadataCallback` which is called per-date:
544
+
545
+ ```javascript
546
+ const picker = document.querySelector('web-daterangepicker');
547
+
548
+ picker.beforeMonthChangedCallback = async (context) => {
549
+ // context: { year, month, monthIndex, firstVisibleDate, lastVisibleDate }
550
+
551
+ // Fetch availability for all visible dates in one call.
552
+ // Use toLocalISO (see "Working with Dates Across Timezones") so the
553
+ // server receives the calendar dates the user actually sees.
554
+ const response = await fetch('/api/availability', {
555
+ method: 'POST',
556
+ body: JSON.stringify({
557
+ start: toLocalISO(context.firstVisibleDate),
558
+ end: toLocalISO(context.lastVisibleDate)
559
+ })
560
+ });
561
+ const data = await response.json();
562
+
563
+ // Build metadata map (key: YYYY-MM-DD, value: DateInfo)
564
+ const metadata = new Map();
565
+ data.forEach(day => {
566
+ metadata.set(day.date, {
567
+ badgeText: `$${day.price}`,
568
+ isDisabled: !day.available,
569
+ dayTooltip: `${day.roomsLeft} rooms available`
570
+ });
571
+ });
572
+
573
+ return { action: 'accept', metadata };
574
+ };
575
+ ```
576
+
577
+ **Return object options:**
578
+
579
+ | Property | Type | Description |
580
+ |----------|------|-------------|
581
+ | `action` | `'accept' \| 'block'` | **Required.** Allow or prevent month navigation |
582
+ | `metadata` | `Map<string, DateInfo>` | Bulk metadata keyed by `YYYY-MM-DD`. Cached and used instead of `getDateMetadataCallback` |
583
+ | `monthHeaders` | `Map<string, string>` | Custom month headers keyed by `YYYY-MM` (e.g., `"2026-01"` → `"Jan 2026 (5 rooms)"`) |
584
+ | `message` | `string` | Optional message to display (useful with `action: 'block'`) |
585
+
586
+ **Performance:** 1 API call per month navigation vs 35-42 calls with `getDateMetadataCallback`.
587
+
588
+ ### Messages & Custom Actions
589
+
590
+ Display contextual messages in the calendar with interactive buttons:
591
+
592
+ ```javascript
593
+ const picker = document.querySelector('web-daterangepicker');
594
+
595
+ // Show a simple message
596
+ picker.showMessage('<p>Please select a check-in date</p>');
597
+
598
+ // Show a message with a close button
599
+ picker.showMessage(`
600
+ <p>Weekend dates have higher rates</p>
601
+ <button data-action="close-message">Got it</button>
602
+ `);
603
+
604
+ // Show a message with custom action buttons
605
+ picker.showMessage(`
606
+ <p>These dates are unavailable. Try:</p>
607
+ <button data-action="custom" data-start-date="2026-01-14" data-end-date="2026-01-17">
608
+ Jan 14 - Jan 17
609
+ </button>
610
+ `);
611
+
612
+ // Handle custom action button clicks
613
+ picker.addEventListener('custom-action', (e) => {
614
+ const { startDate, endDate } = e.detail;
615
+ if (startDate && endDate) {
616
+ picker.selectedRanges = [{
617
+ start: new Date(startDate),
618
+ end: new Date(endDate)
619
+ }];
620
+ picker.hideMessage();
621
+ }
622
+ });
623
+
624
+ // Hide message programmatically
625
+ picker.hideMessage();
626
+ ```
627
+
628
+ **Built-in button actions:**
629
+ - `data-action="close-message"` - Closes the message (no event fired)
630
+ - `data-action="custom"` - Fires `custom-action` event with all `data-*` attributes
631
+
632
+ ## Range Selection Modes
633
+
634
+ When selecting date ranges that include disabled dates (e.g., selecting a working week where weekends are disabled), you can control how the selection is handled using the `disabled-dates-handling` attribute:
635
+
636
+ ### Mode: 'allow' (default)
637
+
638
+ Allows range selections over disabled dates. Returns both enabled and disabled date arrays:
639
+
640
+ ```html
641
+ <web-daterangepicker
642
+ selection-mode="range"
643
+ disabled-weekdays="0,6"
644
+ disabled-dates-handling="allow">
645
+ </web-daterangepicker>
646
+
647
+ <script>
648
+ picker.addEventListener('date-select', (e) => {
649
+ console.log('Enabled dates:', e.detail.enabledDates);
650
+ console.log('Disabled dates:', e.detail.disabledDates);
651
+ console.log('Total days:', e.detail.getTotalDays());
652
+ console.log('Enabled count:', e.detail.getEnabledDateCount());
653
+ });
654
+ </script>
655
+ ```
656
+
657
+ **Use case:** Selecting working weeks where you need to know both working days and weekends (e.g., "Select 3 weeks of work" where weekends are included in the range but you get a separate array of working days).
658
+
659
+ ### Mode: 'prevent'
660
+
661
+ Prevents selecting disabled dates entirely. Clicking a disabled date does nothing:
662
+
663
+ ```html
664
+ <web-daterangepicker
665
+ selection-mode="range"
666
+ disabled-dates-handling="prevent">
667
+ </web-daterangepicker>
668
+ ```
669
+
670
+ **Use case:** Strict date selection where disabled dates should never be part of any selection.
671
+
672
+ ### Mode: 'block'
673
+
674
+ Prevents range selections from crossing disabled dates. Automatically snaps to the last enabled date before the gap:
675
+
676
+ ```html
677
+ <web-daterangepicker
678
+ selection-mode="range"
679
+ disabled-dates-handling="block">
680
+ </web-daterangepicker>
681
+ ```
682
+
683
+ When dragging from day 1 to day 7 with days 4-5 disabled, the selection will automatically snap to days 1-3.
684
+
685
+ **Use case:** Cottage booking where you can't book across existing reservations, or any scenario where gaps in the range are not allowed.
686
+
687
+ ### Mode: 'split'
688
+
689
+ Returns multiple date ranges separated by disabled dates:
690
+
691
+ ```html
692
+ <web-daterangepicker
693
+ selection-mode="range"
694
+ disabled-weekdays="0,6"
695
+ disabled-dates-handling="split">
696
+ </web-daterangepicker>
697
+
698
+ <script>
699
+ picker.addEventListener('date-select', (e) => {
700
+ console.log('Ranges:', e.detail.dateRanges);
701
+ // e.g., [{start: Mon, end: Fri}, {start: Mon, end: Fri}, {start: Mon, end: Fri}]
702
+ console.log('Formatted:', e.detail.formattedValue);
703
+ // "2025-11-03 - 2025-11-07, 2025-11-10 - 2025-11-14, 2025-11-17 - 2025-11-21"
704
+ });
705
+ </script>
706
+ ```
707
+
708
+ **Use case:** Reporting or analytics where you need distinct time periods (e.g., "Generate report for these 3 work weeks").
709
+
710
+ ### Mode: 'individual'
711
+
712
+ Returns a flat array of individual enabled dates:
713
+
714
+ ```html
715
+ <web-daterangepicker
716
+ selection-mode="range"
717
+ disabled-weekdays="0,6"
718
+ disabled-dates-handling="individual">
719
+ </web-daterangepicker>
720
+
721
+ <script>
722
+ picker.addEventListener('date-select', (e) => {
723
+ console.log('Individual dates:', e.detail.dates);
724
+ // [Date(Mon), Date(Tue), Date(Wed), Date(Thu), Date(Fri), Date(Mon), ...]
725
+ console.log('Formatted:', e.detail.formattedValue);
726
+ // "2025-11-03, 2025-11-04, 2025-11-05, 2025-11-06, 2025-11-07, ..."
727
+ });
728
+ </script>
729
+ ```
730
+
731
+ **Use case:** Scheduling or event planning where you need a list of specific dates (e.g., "Schedule training sessions on these dates").
732
+
733
+ ### Event Detail Structure by Mode
734
+
735
+ | Mode | Properties | Description |
736
+ |------|-----------|-------------|
737
+ | `allow` | `dateRange`, `enabledDates`, `disabledDates`, `getTotalDays()`, `getEnabledDateCount()` | Full range with helper methods |
738
+ | `prevent` | `dateRange`, `dates` | Only enabled dates can be selected |
739
+ | `block` | `dateRange`, `dates` | Single continuous range (no disabled dates) |
740
+ | `split` | `dateRanges`, `dates` | Multiple ranges split by disabled dates |
741
+ | `individual` | `dates` | Flat array of enabled dates |
742
+
743
+ ### Visual Highlighting Control
744
+
745
+ By default, when you select a range that includes disabled dates, all dates (both enabled and disabled) within the range are visually highlighted. You can change this behavior with the `highlight-disabled-in-range` attribute:
746
+
747
+ ```html
748
+ <!-- Default: highlights all dates in range, including disabled weekends -->
749
+ <web-daterangepicker
750
+ selection-mode="range"
751
+ disabled-weekdays="0,6"
752
+ disabled-dates-handling="split">
753
+ </web-daterangepicker>
754
+
755
+ <!-- Only highlight enabled dates (Mon-Fri), skip weekends -->
756
+ <web-daterangepicker
757
+ selection-mode="range"
758
+ disabled-weekdays="0,6"
759
+ disabled-dates-handling="split"
760
+ highlight-disabled-in-range="false">
761
+ </web-daterangepicker>
762
+ ```
763
+
764
+ **When to use `highlight-disabled-in-range="false"`:**
765
+ - Selecting working weeks where you only want to see Monday-Friday highlighted
766
+ - Visual clarity when disabled dates are not relevant to the selection
767
+ - Any scenario where showing gaps in the range is clearer than showing continuous highlighting
768
+
769
+ ## Theming
770
+
771
+ ### Theme Designer
772
+
773
+ The easiest way to customize the appearance of this component is using the **KeenMate Theme Designer** at:
774
+
775
+ **[theme-designer.keenmate.dev](https://theme-designer.keenmate.dev)**
776
+
777
+ #### How It Works
778
+
779
+ 1. **Choose 3 base colors** - background, text, and accent
780
+ 2. **Preview changes live** - see your theme applied instantly
781
+ 3. **Fine-tune individual variables** - lock specific values while adjusting others
782
+ 4. **Export your theme** - copy CSS, JSON, or SCSS to your project
783
+
784
+ #### CSS Variable Layers
785
+
786
+ KeenMate components support a **two-layer theming architecture**:
787
+
788
+ **Standalone Mode (Simple)** - Just override the component-specific variables you need:
789
+
790
+ ```css
791
+ :root {
792
+ --drp-accent-color: #your-brand-color;
793
+ --drp-primary-bg: #your-background;
794
+ --drp-text-primary: #your-text-color;
795
+ }
796
+ ```
797
+
798
+ **Cascading Mode (Multi-Component)** - When using multiple KeenMate components, you can define a shared base layer:
799
+
800
+ ```css
801
+ :root {
802
+ /* Base layer - single source of truth */
803
+ --base-accent-color: #3b82f6;
804
+ --base-main-bg: #ffffff;
805
+ --base-hover-bg: #f3f4f6; /* drives hover surfaces across components */
806
+ --base-text-color-1: #111827;
807
+
808
+ /* Components reference base layer */
809
+ --ms-accent-color: var(--base-accent-color);
810
+ --drp-accent-color: var(--base-accent-color);
811
+ }
812
+ ```
813
+
814
+ Change `--base-accent-color` once all components update automatically.
815
+
816
+ #### Unified Variable Naming
817
+
818
+ All KeenMate components follow a consistent naming convention for **Tier 1 variables** (core theming):
819
+
820
+ | Purpose | web-multiselect | web-daterangepicker |
821
+ |---------|-----------------|---------------------|
822
+ | Brand color | `--ms-accent-color` | `--drp-accent-color` |
823
+ | Background | `--ms-primary-bg` | `--drp-primary-bg` |
824
+ | Text color | `--ms-text-primary` | `--drp-text-primary` |
825
+ | Text on accent | `--ms-text-on-accent` | `--drp-text-on-accent` |
826
+ | Border color | `--ms-border-color` | `--drp-border-color` |
827
+
828
+ Learn the pattern once, apply it across all components.
829
+
830
+ #### Component Variables Manifest
831
+
832
+ This package exports a `component-variables.manifest.json` file that documents all supported CSS variables for tooling integration (e.g., Theme Designer, IDE autocomplete):
833
+
834
+ ```javascript
835
+ import manifest from '@keenmate/web-daterangepicker/component-variables.manifest.json';
836
+ // manifest.baseVariables - list of --base-* variables the component responds to
837
+ // manifest.componentVariables - list of --drp-* component-specific variables
838
+ ```
839
+
840
+ #### CSS Cascade Layers
841
+
842
+ The component's stylesheet declares three cascade layers:
843
+
844
+ ```css
845
+ @layer variables, component, overrides;
846
+ ```
847
+
848
+ | Layer | Holds | Priority |
849
+ |-------|-------|----------|
850
+ | `variables` | `:host { --drp-* }` declarations | lowest |
851
+ | `component` | Base rules + every feature partial | middle |
852
+ | `overrides` | Framework-class + per-instance `data-theme` blocks | highest |
853
+
854
+ **Consumer override contract:**
855
+ - Any unlayered consumer rule beats every rule in the component — no `!important` needed.
856
+ - Any `:root { --base-X: }` declaration beats the `variables` layer trivially.
857
+ - To override the dark-mode blocks (the `overrides` layer), use your own `@layer overrides { … }` or any unlayered rule.
858
+
859
+ #### Dark Mode
860
+
861
+ The picker reacts to five separate dark-mode signals — set any one and the dropdown, text, borders, tooltip, and input flip to their dark palette. No JavaScript involved.
862
+
863
+ | Signal | How to trigger |
864
+ |--------|----------------|
865
+ | **OS preference** | `<html style="color-scheme: light dark">` on the consumer page — `light-dark()` then picks the OS branch |
866
+ | **Page-level scheme** | `<body style="color-scheme: dark">` |
867
+ | **Framework class on an ancestor** | `<html data-theme="dark">`, `<html data-bs-theme="dark">` (Bootstrap 5.3+), or `<html class="dark">` (Tailwind) |
868
+ | **Per-instance attribute** | `<web-daterangepicker data-theme="dark">` on a single component |
869
+ | **Explicit light override** | `data-theme="light"`, `data-bs-theme="light"`, or `.light` to force one widget back to light on an otherwise-dark page |
870
+
871
+ `color-scheme` is deliberately NOT declared on `:host` — that would block the page's setting from inheriting into the shadow DOM. See the comment block at the top of `src/css/variables.css` for the rationale.
872
+
873
+ ### CSS Custom Properties
874
+
875
+ Customize the appearance using CSS custom properties:
876
+
877
+ ```css
878
+ :root {
879
+ /* Base Unit - scale entire component by changing this */
880
+ --drp-rem: 10px; /* Default base unit (change to scale everything) */
881
+
882
+ /* Colors */
883
+ --drp-dropdown-background: #ffffff;
884
+ --drp-border-color: #e5e7eb;
885
+ --drp-primary-bg: #f3f4f6;
886
+ --drp-primary-bg-hover: #e5e7eb;
887
+ --drp-accent-color: #3b82f6;
888
+ --drp-accent-color-hover: #2563eb;
889
+ --drp-text-primary: #111827;
890
+ --drp-text-secondary: #6b7280;
891
+ --drp-text-on-accent: #ffffff;
892
+
893
+ /* Input Field */
894
+ --drp-input-background: var(--drp-dropdown-background);
895
+ --drp-input-color: var(--drp-text-primary);
896
+ --drp-input-border: var(--base-input-border, var(--drp-border));
897
+ --drp-input-border-hover: var(--base-input-border-hover, var(--drp-border-width-base) solid var(--drp-accent-color));
898
+ --drp-input-border-focus: var(--base-input-border-focus, var(--drp-border-width-base) solid var(--drp-accent-color));
899
+ --drp-input-placeholder-color: var(--drp-text-secondary);
900
+
901
+ /* Typography (all scale with --drp-rem) */
902
+ --drp-font-size-xs: calc(1.2 * var(--drp-rem)); /* 12px */
903
+ --drp-font-size-sm: calc(1.4 * var(--drp-rem)); /* 14px */
904
+ --drp-font-size-base: calc(1.6 * var(--drp-rem)); /* 16px */
905
+ --drp-font-weight-medium: 500;
906
+ --drp-font-weight-semibold: 600;
907
+
908
+ /* Spacing (all scale with --drp-rem) */
909
+ --drp-spacing-xs: calc(0.4 * var(--drp-rem)); /* 4px */
910
+ --drp-spacing-sm: calc(0.8 * var(--drp-rem)); /* 8px */
911
+ --drp-spacing-md: calc(1.6 * var(--drp-rem)); /* 16px */
912
+
913
+ /* Borders */
914
+ --drp-border-width-base: 1px;
915
+ --drp-border-radius-sm: calc(var(--base-border-radius-sm, 0.4) * var(--drp-rem)); /* 4px - day cells, tooltips */
916
+ --drp-border-radius-md: calc(var(--base-border-radius-md, 0.6) * var(--drp-rem)); /* 6px - inputs, buttons */
917
+ --drp-border-radius-lg: calc(var(--base-border-radius-lg, 0.8) * var(--drp-rem)); /* 8px - calendar, dropdowns */
918
+ --drp-border-radius: var(--drp-border-radius-md); /* default alias */
919
+
920
+ /* Shadows */
921
+ --drp-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
922
+
923
+ /* Transitions */
924
+ --drp-transition-fast: 150ms;
925
+ --drp-easing-snappy: cubic-bezier(0.4, 0.0, 0.2, 1);
926
+ }
927
+ ```
928
+
929
+ ### Input Size Scale
930
+
931
+ The component uses a 10px-based sizing system (`--drp-rem: 10px`) for clean, predictable dimensions:
932
+
933
+ | Size | Attribute | Height | Base Variable |
934
+ |------|-----------|--------|---------------|
935
+ | XS | `input-size="xs"` | 31px | `--base-input-size-xs-height` |
936
+ | SM | `input-size="sm"` | 33px | `--base-input-size-sm-height` |
937
+ | MD | `input-size="md"` | 35px (default) | `--base-input-size-md-height` |
938
+ | LG | `input-size="lg"` | 38px | `--base-input-size-lg-height` |
939
+ | XL | `input-size="xl"` | 41px | `--base-input-size-xl-height` |
940
+
941
+ **Theme Designer Integration**: Input heights reference `--base-input-size-*-height` variables from the [Theme Designer](https://theme-designer.keenmate.dev). This ensures consistent input heights across all KeenMate components (web-multiselect, web-daterangepicker).
942
+
943
+ For complete size variable reference (font sizes, padding, spacing), see [SIZES.md](SIZES.md).
944
+
945
+ #### Customizing Input Heights
946
+
947
+ Four ways to customize input dimensions:
948
+
949
+ ```css
950
+ /* Option 1: Via Theme Designer base variables (recommended for multi-component consistency) */
951
+ :root {
952
+ --base-input-size-md-height: 4.2; /* All components: 42px at 10px rem */
953
+ }
954
+
955
+ /* Option 2: Direct px override */
956
+ web-daterangepicker {
957
+ --drp-input-size-md-height: 42px;
958
+ }
959
+
960
+ /* Option 3: Scale all sizes via --drp-rem */
961
+ web-daterangepicker {
962
+ --drp-rem: 12px; /* MD = 3.5 × 12 = 42px */
963
+ }
964
+
965
+ /* Option 4: Override specific size with calc */
966
+ web-daterangepicker {
967
+ --drp-input-size-md-height: calc(4.2 * var(--drp-rem));
968
+ }
969
+ ```
970
+
971
+ ### Calendar Scaling
972
+
973
+ Scale the entire calendar by setting `--drp-rem` directly on the `<web-daterangepicker>` element:
974
+
975
+ ```css
976
+ /* Compact size (80%) */
977
+ web-daterangepicker.compact {
978
+ --drp-rem: 8px;
979
+ }
980
+
981
+ /* Large size (150%) */
982
+ web-daterangepicker.large {
983
+ --drp-rem: 15px;
984
+ }
985
+
986
+ /* Or use inline style */
987
+ <web-daterangepicker style="--drp-rem: 12px;"></web-daterangepicker>
988
+ ```
989
+
990
+ **Important:** Due to Shadow DOM, CSS variables must be set on the `<web-daterangepicker>` element itself (via class or inline style), not on a wrapper div.
991
+
992
+ For fine-grained control, override individual variables:
993
+
994
+ ```css
995
+ web-daterangepicker.custom {
996
+ --drp-rem: 12px;
997
+ --drp-spacing-xs: 2px; /* Tighter gaps */
998
+ --drp-font-size-base: 18px; /* Larger text */
999
+ }
1000
+ ```
1001
+
1002
+ See [examples-sizes.html](examples-sizes.html) for interactive demos.
1003
+
1004
+ ## Development
1005
+
1006
+ ```bash
1007
+ # Install dependencies
1008
+ npm install
1009
+
1010
+ # Start dev server
1011
+ npm run dev
1012
+
1013
+ # Build for production
1014
+ npm run build
1015
+
1016
+ # Preview production build
1017
+ npm run preview
1018
+ ```
1019
+
1020
+ ## Browser Support
1021
+
1022
+ - Modern browsers with Web Components and CSS `color-mix()` support
1023
+ - Chrome/Edge 111+
1024
+ - Firefox 113+
1025
+ - Safari 16.2+
1026
+
1027
+ For older browser support, use the compiled `dist/style.css` which is processed by Vite.
1028
+
1029
+ ## HTML Injection (XSS) Notice
1030
+
1031
+ The following callbacks and methods allow **raw HTML injection** and are intentionally **NOT XSS-safe**. This gives developers full control over rendering but requires sanitizing untrusted data:
1032
+
1033
+ | Callback/Method | Output Used In | Risk Level |
1034
+ |-----------------|---------------|------------|
1035
+ | `showMessage(html)` | Message area (innerHTML) | HTML injection |
1036
+ | `renderDayCallback` | Day cells (innerHTML) | HTML injection |
1037
+ | `renderDayContentCallback` | Day cells (innerHTML) | HTML injection |
1038
+ | `getDateMetadataCallback` (badgeText, dayTooltip) | Badges/tooltips (innerHTML) | HTML injection |
1039
+ | `formatSummaryCallback` | Summary display (innerHTML) | HTML injection |
1040
+ | `getMonthHeaderCallback` | Month headers (innerHTML) | HTML injection |
1041
+ | `getUnifiedHeaderCallback` | Unified header (innerHTML) | HTML injection |
1042
+ | `customStylesCallback` | Style tag (textContent) | CSS injection |
1043
+ | `actionButtons[].label` | Button labels (innerHTML) | HTML injection |
1044
+
1045
+ **Safe callbacks** (output is escaped or used as data):
1046
+ - `beforeDateSelectCallback`, `beforeMonthChangedCallback` (return action objects)
1047
+ - `onSelect`, `onChange` (event handlers)
1048
+ - `getDateMetadataCallback` (isDisabled, dayClass, badgeClass - CSS class names only)
1049
+
1050
+ **If displaying user-generated content**, sanitize it before passing to these callbacks or methods.
1051
+
1052
+ ## Changelog
1053
+
1054
+ See [CHANGELOG.md](https://github.com/keenmate/web-daterangepicker/blob/main/CHANGELOG.md) for version history and migration guides.
1055
+
1056
+ ## License
1057
+
1058
+ MIT
1059
+
1060
+ ## Credits
1061
+
1062
+ Extracted from the [Pure Admin](https://github.com/keenmate/pure-admin) design system.