@reliability-design/web 1.0.3 → 1.0.6

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 (114) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +43 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +18 -7
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-badge/README.md +86 -0
  7. package/dist/components/rel-badge/rel-badge.ts +6 -6
  8. package/dist/components/rel-breadcrumb/README.md +102 -0
  9. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  10. package/dist/components/rel-button/README.md +130 -0
  11. package/dist/components/rel-calendar/README.md +109 -0
  12. package/dist/components/rel-calendar/index.ts +1 -0
  13. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  14. package/dist/components/rel-card/README.md +94 -0
  15. package/dist/components/rel-card/rel-card.ts +12 -3
  16. package/dist/components/rel-cardmedia/README.md +127 -0
  17. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  18. package/dist/components/rel-carousel/README.md +166 -0
  19. package/dist/components/rel-carousel/index.ts +1 -0
  20. package/dist/components/rel-carousel/rel-carousel.ts +469 -0
  21. package/dist/components/rel-chart/README.md +286 -0
  22. package/dist/components/rel-chart/index.ts +1 -0
  23. package/dist/components/rel-chart/rel-chart.ts +736 -0
  24. package/dist/components/rel-checkbox/README.md +77 -0
  25. package/dist/components/rel-checkbox/rel-checkbox.ts +18 -10
  26. package/dist/components/rel-chip/README.md +114 -0
  27. package/dist/components/rel-chip/rel-chip.ts +20 -4
  28. package/dist/components/rel-container/README.md +99 -0
  29. package/dist/components/rel-date-picker/README.md +253 -0
  30. package/dist/components/rel-date-picker/index.ts +1 -0
  31. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  32. package/dist/components/rel-divider/README.md +100 -0
  33. package/dist/components/rel-drawer/README.md +200 -0
  34. package/dist/components/rel-drawer/index.ts +1 -0
  35. package/dist/components/rel-drawer/rel-drawer.ts +621 -0
  36. package/dist/components/rel-kbd/README.md +151 -0
  37. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  38. package/dist/components/rel-layout/README.md +88 -0
  39. package/dist/components/rel-menu/README.md +151 -0
  40. package/dist/components/rel-menu/rel-menu.ts +18 -10
  41. package/dist/components/rel-menubar/README.md +329 -0
  42. package/dist/components/rel-menubar/index.ts +2 -0
  43. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  44. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  45. package/dist/components/rel-nav-menu/README.md +289 -0
  46. package/dist/components/rel-nav-menu/index.ts +3 -0
  47. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  48. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  49. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  50. package/dist/components/rel-navbar/README.md +126 -0
  51. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  52. package/dist/components/rel-otp/README.md +440 -0
  53. package/dist/components/rel-otp/index.ts +1 -0
  54. package/dist/components/rel-otp/rel-otp.ts +504 -0
  55. package/dist/components/rel-pagination/README.md +410 -0
  56. package/dist/components/rel-pagination/index.ts +1 -0
  57. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  58. package/dist/components/rel-popover/README.md +568 -0
  59. package/dist/components/rel-popover/index.ts +2 -0
  60. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  61. package/dist/components/rel-popover/rel-popover.ts +327 -0
  62. package/dist/components/rel-progress/README.md +110 -0
  63. package/dist/components/rel-progress/rel-progress.ts +32 -5
  64. package/dist/components/rel-quote-page/README.md +166 -0
  65. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  66. package/dist/components/rel-radio/README.md +120 -0
  67. package/dist/components/rel-radio/rel-radio.ts +13 -5
  68. package/dist/components/rel-resizable/README.md +140 -0
  69. package/dist/components/rel-resizable/index.ts +3 -0
  70. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  71. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  72. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  73. package/dist/components/rel-segmented-button/README.md +141 -0
  74. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  75. package/dist/components/rel-select/README.md +107 -0
  76. package/dist/components/rel-select/rel-select.ts +14 -14
  77. package/dist/components/rel-sheet/README.md +224 -0
  78. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  79. package/dist/components/rel-sidebar/README.md +209 -0
  80. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  81. package/dist/components/rel-slider/README.md +135 -0
  82. package/dist/components/rel-slider/rel-slider.ts +19 -7
  83. package/dist/components/rel-snackbar/README.md +134 -0
  84. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
  85. package/dist/components/rel-stepper/README.md +160 -0
  86. package/dist/components/rel-stepper/rel-stepper.ts +6 -6
  87. package/dist/components/rel-switch/README.md +102 -0
  88. package/dist/components/rel-switch/rel-switch.ts +15 -6
  89. package/dist/components/rel-tab/README.md +163 -0
  90. package/dist/components/rel-tab/rel-tab.ts +36 -14
  91. package/dist/components/rel-table/README.md +178 -0
  92. package/dist/components/rel-table/rel-table-body.ts +16 -0
  93. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  94. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  95. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  96. package/dist/components/rel-table/rel-table-head.ts +33 -0
  97. package/dist/components/rel-table/rel-table-header.ts +16 -0
  98. package/dist/components/rel-table/rel-table-row.ts +32 -0
  99. package/dist/components/rel-table/rel-table.ts +47 -0
  100. package/dist/components/rel-textfield/README.md +90 -0
  101. package/dist/components/rel-textfield/rel-textfield.ts +35 -19
  102. package/dist/components/rel-toast/README.md +148 -0
  103. package/dist/components/rel-toast/rel-toast.ts +32 -14
  104. package/dist/components/rel-tooltip/README.md +103 -0
  105. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  106. package/dist/index.d.mts +836 -1
  107. package/dist/index.d.ts +836 -1
  108. package/dist/index.js +10658 -1493
  109. package/dist/index.js.map +1 -1
  110. package/dist/index.mjs +10616 -1499
  111. package/dist/index.mjs.map +1 -1
  112. package/dist/styles/vars.css +293 -183
  113. package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
  114. package/package.json +1 -1
@@ -0,0 +1,916 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+
4
+ interface CalendarDay {
5
+ date: Date;
6
+ day: number;
7
+ isCurrentMonth: boolean;
8
+ isToday: boolean;
9
+ isSelected: boolean;
10
+ isDisabled: boolean;
11
+ }
12
+
13
+ @customElement("rel-date-picker")
14
+ export class RelDatePicker extends LitElement {
15
+ @property({ type: String, reflect: true }) value: string | null = null;
16
+ @property({ type: String }) placeholder = "Select date";
17
+ @property({ type: String }) label: string | null = null;
18
+ @property({ type: Boolean, reflect: true }) disabled = false;
19
+ @property({ type: Boolean }) required = false;
20
+ @property({ type: String }) min: string | null = null;
21
+ @property({ type: String }) max: string | null = null;
22
+ @property({ type: String, attribute: "week-start" }) weekStart: "sun" | "mon" = "sun";
23
+ @property({ type: String }) locale = "en-US";
24
+ @property({ type: Boolean, reflect: true }) open = false;
25
+ @property({ type: Boolean, attribute: "close-on-select" }) closeOnSelect = true;
26
+ @property({ type: Boolean }) clearable = false;
27
+
28
+ // Custom disabled predicate (not settable via attribute)
29
+ public isDateDisabled?: (date: Date) => boolean;
30
+
31
+ @state() private viewYear: number = new Date().getFullYear();
32
+ @state() private viewMonth: number = new Date().getMonth();
33
+ @state() private focusedDate: Date | null = null;
34
+ @state() private yearDropdownOpen = false;
35
+ @state() private monthDropdownOpen = false;
36
+
37
+ @query(".trigger-button") private triggerButton?: HTMLButtonElement;
38
+ @query(".popover") private popoverElement?: HTMLElement;
39
+ @query(".calendar-grid") private calendarGrid?: HTMLElement;
40
+
41
+ static styles = css`
42
+ :host {
43
+ display: inline-block;
44
+ font-family: var(--rel-fontFamilies-primary);
45
+ position: relative;
46
+ }
47
+
48
+ :host([disabled]) {
49
+ opacity: 0.6;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .date-picker-wrapper {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--rel-spacing-space-xs);
57
+ }
58
+
59
+ .label {
60
+ font-size: var(--rel-fontSizes-sm);
61
+ font-weight: var(--rel-fontWeights-medium);
62
+ color: var(--rel-text-heading);
63
+ }
64
+
65
+ .trigger-button {
66
+ width: 100%;
67
+ min-width: 200px;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ gap: var(--rel-spacing-space-xs);
72
+ padding: var(--rel-spacing-space-sm) var(--rel-spacing-space-md);
73
+ background: var(--rel-colors-white);
74
+ border: 1px solid var(--rel-border-default);
75
+ border-radius: var(--rel-borders-radius-border-radius-200);
76
+ font-family: var(--rel-fontFamilies-primary);
77
+ font-size: var(--rel-fontSizes-md);
78
+ color: var(--rel-text-primary);
79
+ cursor: pointer;
80
+ transition: all 0.15s ease;
81
+ text-align: left;
82
+ }
83
+
84
+ .trigger-button:hover:not(:disabled) {
85
+ border-color: var(--rel-colors-gray-heading);
86
+ }
87
+
88
+ .trigger-button:focus-visible {
89
+ outline: 2px solid var(--rel-colors-blue-primary);
90
+ outline-offset: 2px;
91
+ border-color: var(--rel-colors-blue-primary);
92
+ }
93
+
94
+ .trigger-button:disabled {
95
+ cursor: not-allowed;
96
+ background: var(--rel-surface-secondary);
97
+ }
98
+
99
+ .trigger-content {
100
+ flex: 1;
101
+ color: var(--rel-text-primary);
102
+ }
103
+
104
+ .trigger-content.placeholder {
105
+ color: var(--rel-colors-gray-placeholder);
106
+ }
107
+
108
+ .trigger-icons {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: var(--rel-spacing-space-2xs);
112
+ }
113
+
114
+ .icon-button {
115
+ background: none;
116
+ border: none;
117
+ padding: var(--rel-spacing-space-3xs);
118
+ cursor: pointer;
119
+ color: var(--rel-colors-gray-placeholder);
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ border-radius: var(--rel-borders-radius-border-radius-100);
124
+ transition: all 0.15s ease;
125
+ }
126
+
127
+ .icon-button:hover {
128
+ background: var(--rel-surface-secondary);
129
+ color: var(--rel-colors-gray-heading);
130
+ }
131
+
132
+ .icon-button svg {
133
+ width: 16px;
134
+ height: 16px;
135
+ }
136
+
137
+ .popover {
138
+ position: absolute;
139
+ top: calc(100% + 4px);
140
+ left: 0;
141
+ z-index: 1000;
142
+ background: var(--rel-colors-white);
143
+ border: 1px solid var(--rel-border-default);
144
+ border-radius: var(--rel-borders-radius-border-radius-200);
145
+ box-shadow: var(--rel-shadows-shadow-300);
146
+ padding: var(--rel-spacing-space-md);
147
+ min-width: 280px;
148
+ display: none;
149
+ }
150
+
151
+ .popover.open {
152
+ display: block;
153
+ }
154
+
155
+ .calendar-header {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ margin-bottom: var(--rel-spacing-space-md);
160
+ gap: var(--rel-spacing-space-xs);
161
+ }
162
+
163
+ .nav-button {
164
+ background: transparent;
165
+ border: none;
166
+ width: 32px;
167
+ height: 32px;
168
+ border-radius: var(--rel-borders-radius-border-radius-200);
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ cursor: pointer;
173
+ color: var(--rel-colors-gray-primary);
174
+ transition: background-color 0.15s ease;
175
+ }
176
+
177
+ .nav-button:hover {
178
+ background: var(--rel-surface-secondary);
179
+ }
180
+
181
+ .nav-button:focus-visible {
182
+ outline: 2px solid var(--rel-colors-blue-primary);
183
+ outline-offset: 2px;
184
+ }
185
+
186
+ .dropdown-container {
187
+ position: relative;
188
+ flex: 1;
189
+ }
190
+
191
+ .dropdown-trigger {
192
+ width: 100%;
193
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
194
+ background: var(--rel-colors-white);
195
+ border: 1px solid var(--rel-border-default);
196
+ border-radius: var(--rel-borders-radius-border-radius-200);
197
+ font-family: var(--rel-fontFamilies-primary);
198
+ font-size: var(--rel-fontSizes-sm);
199
+ font-weight: var(--rel-fontWeights-medium);
200
+ color: var(--rel-text-heading);
201
+ cursor: pointer;
202
+ text-align: center;
203
+ transition: all 0.15s ease;
204
+ }
205
+
206
+ .dropdown-trigger:hover {
207
+ background: var(--rel-surface-secondary);
208
+ }
209
+
210
+ .dropdown-trigger:focus-visible {
211
+ outline: 2px solid var(--rel-colors-blue-primary);
212
+ outline-offset: 2px;
213
+ }
214
+
215
+ .dropdown-menu {
216
+ position: absolute;
217
+ top: calc(100% + 4px);
218
+ left: 0;
219
+ right: 0;
220
+ z-index: 10;
221
+ background: var(--rel-colors-white);
222
+ border: 1px solid var(--rel-border-default);
223
+ border-radius: var(--rel-borders-radius-border-radius-200);
224
+ box-shadow: var(--rel-shadows-shadow-200);
225
+ max-height: 200px;
226
+ overflow-y: auto;
227
+ display: none;
228
+ }
229
+
230
+ .dropdown-menu.open {
231
+ display: block;
232
+ }
233
+
234
+ .dropdown-item {
235
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
236
+ cursor: pointer;
237
+ font-size: var(--rel-fontSizes-sm);
238
+ color: var(--rel-text-primary);
239
+ transition: background-color 0.15s ease;
240
+ }
241
+
242
+ .dropdown-item:hover {
243
+ background: var(--rel-surface-secondary);
244
+ }
245
+
246
+ .dropdown-item.selected {
247
+ background: var(--rel-colors-blue-bg);
248
+ color: var(--rel-colors-blue-primary);
249
+ font-weight: var(--rel-fontWeights-semibold);
250
+ }
251
+
252
+ .weekdays {
253
+ display: grid;
254
+ grid-template-columns: repeat(7, 1fr);
255
+ gap: var(--rel-spacing-space-3xs);
256
+ margin-bottom: var(--rel-spacing-space-xs);
257
+ }
258
+
259
+ .weekday {
260
+ text-align: center;
261
+ font-size: var(--rel-fontSizes-xs);
262
+ font-weight: var(--rel-fontWeights-semibold);
263
+ color: var(--rel-colors-gray-placeholder);
264
+ padding: var(--rel-spacing-space-xs);
265
+ text-transform: uppercase;
266
+ }
267
+
268
+ .calendar-grid {
269
+ display: grid;
270
+ grid-template-columns: repeat(7, 1fr);
271
+ gap: var(--rel-spacing-space-3xs);
272
+ }
273
+
274
+ .day-button {
275
+ aspect-ratio: 1;
276
+ border: none;
277
+ background: transparent;
278
+ border-radius: var(--rel-borders-radius-border-radius-200);
279
+ font-size: var(--rel-fontSizes-sm);
280
+ font-weight: var(--rel-fontWeights-medium);
281
+ color: var(--rel-text-primary);
282
+ cursor: pointer;
283
+ position: relative;
284
+ transition: all 0.15s ease;
285
+ font-family: var(--rel-fontFamilies-primary);
286
+ }
287
+
288
+ .day-button:hover:not(:disabled):not(.selected) {
289
+ background: var(--rel-surface-secondary);
290
+ }
291
+
292
+ .day-button:focus-visible {
293
+ outline: 2px solid var(--rel-colors-blue-primary);
294
+ outline-offset: -2px;
295
+ z-index: 1;
296
+ }
297
+
298
+ .day-button.today:not(.selected) {
299
+ border: 2px solid var(--rel-colors-blue-primary);
300
+ }
301
+
302
+ .day-button.selected {
303
+ background: var(--rel-colors-green-primary);
304
+ color: var(--rel-colors-black);
305
+ font-weight: var(--rel-fontWeights-semibold);
306
+ }
307
+
308
+ .day-button.other-month {
309
+ color: var(--rel-colors-gray-placeholder);
310
+ opacity: 0.5;
311
+ }
312
+
313
+ .day-button:disabled {
314
+ color: var(--rel-text-disabled);
315
+ cursor: not-allowed;
316
+ opacity: 0.5;
317
+ }
318
+
319
+ .day-button:disabled:hover {
320
+ background: transparent;
321
+ }
322
+
323
+ @media (prefers-reduced-motion: reduce) {
324
+ .trigger-button,
325
+ .icon-button,
326
+ .nav-button,
327
+ .dropdown-trigger,
328
+ .dropdown-item,
329
+ .day-button {
330
+ transition: none;
331
+ }
332
+ }
333
+ `;
334
+
335
+ connectedCallback() {
336
+ super.connectedCallback();
337
+ if (this.value) {
338
+ const date = this.parseISODate(this.value);
339
+ if (date) {
340
+ this.viewYear = date.getFullYear();
341
+ this.viewMonth = date.getMonth();
342
+ }
343
+ }
344
+
345
+ // Click outside handler
346
+ document.addEventListener("click", this.handleClickOutside);
347
+ document.addEventListener("keydown", this.handleEscapeKey);
348
+ }
349
+
350
+ disconnectedCallback() {
351
+ super.disconnectedCallback();
352
+ document.removeEventListener("click", this.handleClickOutside);
353
+ document.removeEventListener("keydown", this.handleEscapeKey);
354
+ }
355
+
356
+ private handleClickOutside = (e: MouseEvent) => {
357
+ if (!this.open) return;
358
+
359
+ const path = e.composedPath();
360
+ if (!path.includes(this)) {
361
+ this.closeCalendarPopover();
362
+ }
363
+ };
364
+
365
+ private handleEscapeKey = (e: KeyboardEvent) => {
366
+ if (e.key === "Escape" && this.open) {
367
+ this.closeCalendarPopover();
368
+ }
369
+ };
370
+
371
+ updated(changedProperties: Map<string, any>) {
372
+ if (changedProperties.has("value") && this.value) {
373
+ const date = this.parseISODate(this.value);
374
+ if (date) {
375
+ this.viewYear = date.getFullYear();
376
+ this.viewMonth = date.getMonth();
377
+ }
378
+ }
379
+
380
+ if (changedProperties.has("open") && this.open) {
381
+ // Focus first day when opening
382
+ setTimeout(() => {
383
+ const firstDay = this.calendarGrid?.querySelector<HTMLButtonElement>(
384
+ ".day-button:not(:disabled)"
385
+ );
386
+ firstDay?.focus();
387
+ }, 100);
388
+ }
389
+ }
390
+
391
+ private parseISODate(iso: string): Date | null {
392
+ const match = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
393
+ if (!match) return null;
394
+ const [, year, month, day] = match;
395
+ return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
396
+ }
397
+
398
+ private formatISODate(date: Date): string {
399
+ const year = date.getFullYear();
400
+ const month = String(date.getMonth() + 1).padStart(2, "0");
401
+ const day = String(date.getDate()).padStart(2, "0");
402
+ return `${year}-${month}-${day}`;
403
+ }
404
+
405
+ private formatDisplayDate(iso: string): string {
406
+ const date = this.parseISODate(iso);
407
+ if (!date) return iso;
408
+
409
+ return new Intl.DateTimeFormat(this.locale, {
410
+ year: "numeric",
411
+ month: "long",
412
+ day: "numeric",
413
+ }).format(date);
414
+ }
415
+
416
+ private isSameDay(date1: Date, date2: Date): boolean {
417
+ return (
418
+ date1.getFullYear() === date2.getFullYear() &&
419
+ date1.getMonth() === date2.getMonth() &&
420
+ date1.getDate() === date2.getDate()
421
+ );
422
+ }
423
+
424
+ private isDateDisabledInternal(date: Date): boolean {
425
+ if (this.disabled) return true;
426
+
427
+ if (this.min) {
428
+ const minDate = this.parseISODate(this.min);
429
+ if (minDate && date < minDate) return true;
430
+ }
431
+
432
+ if (this.max) {
433
+ const maxDate = this.parseISODate(this.max);
434
+ if (maxDate && date > maxDate) return true;
435
+ }
436
+
437
+ if (this.isDateDisabled && this.isDateDisabled(date)) {
438
+ return true;
439
+ }
440
+
441
+ return false;
442
+ }
443
+
444
+ private getCalendarDays(): CalendarDay[] {
445
+ const days: CalendarDay[] = [];
446
+ const firstDayOfMonth = new Date(this.viewYear, this.viewMonth, 1);
447
+ const lastDayOfMonth = new Date(this.viewYear, this.viewMonth + 1, 0);
448
+ const today = new Date();
449
+ const selectedDate = this.value ? this.parseISODate(this.value) : null;
450
+
451
+ let startDay = firstDayOfMonth.getDay();
452
+ if (this.weekStart === "mon") {
453
+ startDay = startDay === 0 ? 6 : startDay - 1;
454
+ }
455
+
456
+ // Leading days from previous month
457
+ for (let i = startDay - 1; i >= 0; i--) {
458
+ const date = new Date(this.viewYear, this.viewMonth, -i);
459
+ days.push({
460
+ date,
461
+ day: date.getDate(),
462
+ isCurrentMonth: false,
463
+ isToday: this.isSameDay(date, today),
464
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
465
+ isDisabled: true, // Leading days not selectable
466
+ });
467
+ }
468
+
469
+ // Current month days
470
+ for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
471
+ const date = new Date(this.viewYear, this.viewMonth, day);
472
+ days.push({
473
+ date,
474
+ day,
475
+ isCurrentMonth: true,
476
+ isToday: this.isSameDay(date, today),
477
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
478
+ isDisabled: this.isDateDisabledInternal(date),
479
+ });
480
+ }
481
+
482
+ // Trailing days from next month
483
+ const remainingCells = 42 - days.length; // 6 weeks max
484
+ for (let i = 1; i <= remainingCells; i++) {
485
+ const date = new Date(this.viewYear, this.viewMonth + 1, i);
486
+ days.push({
487
+ date,
488
+ day: date.getDate(),
489
+ isCurrentMonth: false,
490
+ isToday: this.isSameDay(date, today),
491
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
492
+ isDisabled: true, // Trailing days not selectable
493
+ });
494
+ }
495
+
496
+ return days;
497
+ }
498
+
499
+ private getWeekdayLabels(): string[] {
500
+ const formatter = new Intl.DateTimeFormat(this.locale, { weekday: "short" });
501
+ const days = [];
502
+ const baseDate = new Date(2024, 0, 7); // Sunday, Jan 7, 2024
503
+
504
+ for (let i = 0; i < 7; i++) {
505
+ const offset = this.weekStart === "mon" ? i + 1 : i;
506
+ const date = new Date(baseDate);
507
+ date.setDate(baseDate.getDate() + offset);
508
+ days.push(formatter.format(date).substring(0, 2));
509
+ }
510
+
511
+ return days;
512
+ }
513
+
514
+ private getMonthName(): string {
515
+ const formatter = new Intl.DateTimeFormat(this.locale, { month: "long" });
516
+ return formatter.format(new Date(this.viewYear, this.viewMonth, 1));
517
+ }
518
+
519
+ private getMonthsList(): { value: number; label: string }[] {
520
+ const months = [];
521
+ for (let i = 0; i < 12; i++) {
522
+ const formatter = new Intl.DateTimeFormat(this.locale, { month: "long" });
523
+ const label = formatter.format(new Date(2024, i, 1));
524
+ months.push({ value: i, label });
525
+ }
526
+ return months;
527
+ }
528
+
529
+ private getYearsList(): number[] {
530
+ const currentYear = new Date().getFullYear();
531
+ let startYear = currentYear - 100;
532
+ let endYear = currentYear + 10;
533
+
534
+ if (this.min) {
535
+ const minDate = this.parseISODate(this.min);
536
+ if (minDate) startYear = Math.max(startYear, minDate.getFullYear() - 5);
537
+ }
538
+
539
+ if (this.max) {
540
+ const maxDate = this.parseISODate(this.max);
541
+ if (maxDate) endYear = Math.min(endYear, maxDate.getFullYear() + 5);
542
+ }
543
+
544
+ const years = [];
545
+ for (let y = startYear; y <= endYear; y++) {
546
+ years.push(y);
547
+ }
548
+ return years.reverse(); // Most recent first
549
+ }
550
+
551
+ private toggleCalendarPopover() {
552
+ if (this.disabled) return;
553
+ this.open = !this.open;
554
+ this.dispatchEvent(
555
+ new CustomEvent("rel-open-change", {
556
+ detail: { open: this.open },
557
+ bubbles: true,
558
+ composed: true,
559
+ })
560
+ );
561
+ }
562
+
563
+ private closeCalendarPopover() {
564
+ this.open = false;
565
+ this.monthDropdownOpen = false;
566
+ this.yearDropdownOpen = false;
567
+ this.triggerButton?.focus();
568
+ this.dispatchEvent(
569
+ new CustomEvent("rel-open-change", {
570
+ detail: { open: false },
571
+ bubbles: true,
572
+ composed: true,
573
+ })
574
+ );
575
+ }
576
+
577
+ private previousMonth() {
578
+ if (this.viewMonth === 0) {
579
+ this.viewMonth = 11;
580
+ this.viewYear--;
581
+ } else {
582
+ this.viewMonth--;
583
+ }
584
+ }
585
+
586
+ private nextMonth() {
587
+ if (this.viewMonth === 11) {
588
+ this.viewMonth = 0;
589
+ this.viewYear++;
590
+ } else {
591
+ this.viewMonth++;
592
+ }
593
+ }
594
+
595
+ private selectDay(day: CalendarDay) {
596
+ if (day.isDisabled || this.disabled) return;
597
+
598
+ const newValue = this.formatISODate(day.date);
599
+ this.value = newValue;
600
+
601
+ this.dispatchEvent(
602
+ new CustomEvent("rel-change", {
603
+ detail: {
604
+ value: newValue,
605
+ date: new Date(day.date),
606
+ },
607
+ bubbles: true,
608
+ composed: true,
609
+ })
610
+ );
611
+
612
+ if (this.closeOnSelect) {
613
+ this.closeCalendarPopover();
614
+ }
615
+ }
616
+
617
+ private clearValue(e: Event) {
618
+ e.stopPropagation();
619
+ this.value = null;
620
+
621
+ this.dispatchEvent(
622
+ new CustomEvent("rel-change", {
623
+ detail: {
624
+ value: null,
625
+ date: null,
626
+ },
627
+ bubbles: true,
628
+ composed: true,
629
+ })
630
+ );
631
+ }
632
+
633
+ private handleTriggerKeydown(e: KeyboardEvent) {
634
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
635
+ e.preventDefault();
636
+ if (!this.open) {
637
+ this.toggleCalendarPopover();
638
+ }
639
+ }
640
+ }
641
+
642
+ private handleDayKeydown(e: KeyboardEvent, day: CalendarDay, index: number) {
643
+ const days = this.getCalendarDays();
644
+ let targetIndex = index;
645
+
646
+ switch (e.key) {
647
+ case "ArrowLeft":
648
+ e.preventDefault();
649
+ targetIndex = index - 1;
650
+ break;
651
+ case "ArrowRight":
652
+ e.preventDefault();
653
+ targetIndex = index + 1;
654
+ break;
655
+ case "ArrowUp":
656
+ e.preventDefault();
657
+ targetIndex = index - 7;
658
+ break;
659
+ case "ArrowDown":
660
+ e.preventDefault();
661
+ targetIndex = index + 7;
662
+ break;
663
+ case "Home":
664
+ e.preventDefault();
665
+ targetIndex = index - (index % 7);
666
+ break;
667
+ case "End":
668
+ e.preventDefault();
669
+ targetIndex = index - (index % 7) + 6;
670
+ break;
671
+ case "PageUp":
672
+ e.preventDefault();
673
+ if (e.shiftKey) {
674
+ this.viewYear--;
675
+ } else {
676
+ this.previousMonth();
677
+ }
678
+ return;
679
+ case "PageDown":
680
+ e.preventDefault();
681
+ if (e.shiftKey) {
682
+ this.viewYear++;
683
+ } else {
684
+ this.nextMonth();
685
+ }
686
+ return;
687
+ case "Enter":
688
+ case " ":
689
+ e.preventDefault();
690
+ this.selectDay(day);
691
+ return;
692
+ default:
693
+ return;
694
+ }
695
+
696
+ if (targetIndex >= 0 && targetIndex < days.length) {
697
+ const targetDay = days[targetIndex];
698
+ if (!targetDay.isCurrentMonth) {
699
+ if (targetIndex < index) {
700
+ this.previousMonth();
701
+ } else {
702
+ this.nextMonth();
703
+ }
704
+ }
705
+ this.requestUpdate();
706
+ this.updateComplete.then(() => {
707
+ const buttons = this.calendarGrid?.querySelectorAll<HTMLButtonElement>(".day-button");
708
+ if (buttons && buttons[targetIndex]) {
709
+ buttons[targetIndex].focus();
710
+ }
711
+ });
712
+ }
713
+ }
714
+
715
+ private toggleMonthDropdown(e: Event) {
716
+ e.stopPropagation();
717
+ this.monthDropdownOpen = !this.monthDropdownOpen;
718
+ this.yearDropdownOpen = false;
719
+ }
720
+
721
+ private toggleYearDropdown(e: Event) {
722
+ e.stopPropagation();
723
+ this.yearDropdownOpen = !this.yearDropdownOpen;
724
+ this.monthDropdownOpen = false;
725
+ }
726
+
727
+ private selectMonth(month: number, e: Event) {
728
+ e.stopPropagation();
729
+ this.viewMonth = month;
730
+ this.monthDropdownOpen = false;
731
+ }
732
+
733
+ private selectYear(year: number, e: Event) {
734
+ e.stopPropagation();
735
+ this.viewYear = year;
736
+ this.yearDropdownOpen = false;
737
+ }
738
+
739
+ render() {
740
+ const days = this.getCalendarDays();
741
+ const weekdayLabels = this.getWeekdayLabels();
742
+ const months = this.getMonthsList();
743
+ const years = this.getYearsList();
744
+
745
+ return html`
746
+ <div class="date-picker-wrapper">
747
+ ${this.label
748
+ ? html`<label class="label" id="date-picker-label">${this.label}</label>`
749
+ : nothing}
750
+
751
+ <button
752
+ class="trigger-button"
753
+ type="button"
754
+ ?disabled=${this.disabled}
755
+ @click=${this.toggleCalendarPopover}
756
+ @keydown=${this.handleTriggerKeydown}
757
+ aria-label=${this.label || "Select date"}
758
+ aria-haspopup="dialog"
759
+ aria-expanded=${this.open}
760
+ aria-required=${this.required}
761
+ aria-labelledby=${this.label ? "date-picker-label" : nothing}
762
+ >
763
+ <span class="trigger-content ${this.value ? "" : "placeholder"}">
764
+ ${this.value ? this.formatDisplayDate(this.value) : this.placeholder}
765
+ </span>
766
+ <span class="trigger-icons">
767
+ ${this.clearable && this.value
768
+ ? html`
769
+ <button
770
+ class="icon-button"
771
+ type="button"
772
+ aria-label="Clear date"
773
+ @click=${this.clearValue}
774
+ >
775
+ <svg viewBox="0 0 16 16" fill="currentColor">
776
+ <path
777
+ d="M4 4l8 8M12 4l-8 8"
778
+ stroke="currentColor"
779
+ stroke-width="2"
780
+ stroke-linecap="round"
781
+ />
782
+ </svg>
783
+ </button>
784
+ `
785
+ : nothing}
786
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
787
+ <path
788
+ d="M4 2h1V1h2v1h2V1h2v1h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 3H4v7h8V5z"
789
+ />
790
+ </svg>
791
+ </span>
792
+ </button>
793
+
794
+ <div class="popover ${this.open ? "open" : ""}" role="dialog" aria-label="Choose date">
795
+ <div class="calendar-header">
796
+ <button
797
+ class="nav-button"
798
+ @click=${this.previousMonth}
799
+ aria-label="Previous month"
800
+ type="button"
801
+ >
802
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
803
+ <path
804
+ d="M10 12L6 8L10 4"
805
+ stroke="currentColor"
806
+ stroke-width="2"
807
+ fill="none"
808
+ stroke-linecap="round"
809
+ stroke-linejoin="round"
810
+ />
811
+ </svg>
812
+ </button>
813
+
814
+ <div class="dropdown-container">
815
+ <button
816
+ class="dropdown-trigger"
817
+ type="button"
818
+ @click=${this.toggleMonthDropdown}
819
+ aria-label="Select month"
820
+ aria-expanded=${this.monthDropdownOpen}
821
+ >
822
+ ${this.getMonthName()}
823
+ </button>
824
+ <div class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}">
825
+ ${months.map(
826
+ (m) => html`
827
+ <div
828
+ class="dropdown-item ${m.value === this.viewMonth ? "selected" : ""}"
829
+ @click=${(e: Event) => this.selectMonth(m.value, e)}
830
+ role="option"
831
+ aria-selected=${m.value === this.viewMonth}
832
+ >
833
+ ${m.label}
834
+ </div>
835
+ `
836
+ )}
837
+ </div>
838
+ </div>
839
+
840
+ <div class="dropdown-container">
841
+ <button
842
+ class="dropdown-trigger"
843
+ type="button"
844
+ @click=${this.toggleYearDropdown}
845
+ aria-label="Select year"
846
+ aria-expanded=${this.yearDropdownOpen}
847
+ >
848
+ ${this.viewYear}
849
+ </button>
850
+ <div class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}">
851
+ ${years.map(
852
+ (y) => html`
853
+ <div
854
+ class="dropdown-item ${y === this.viewYear ? "selected" : ""}"
855
+ @click=${(e: Event) => this.selectYear(y, e)}
856
+ role="option"
857
+ aria-selected=${y === this.viewYear}
858
+ >
859
+ ${y}
860
+ </div>
861
+ `
862
+ )}
863
+ </div>
864
+ </div>
865
+
866
+ <button
867
+ class="nav-button"
868
+ @click=${this.nextMonth}
869
+ aria-label="Next month"
870
+ type="button"
871
+ >
872
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
873
+ <path
874
+ d="M6 4L10 8L6 12"
875
+ stroke="currentColor"
876
+ stroke-width="2"
877
+ fill="none"
878
+ stroke-linecap="round"
879
+ stroke-linejoin="round"
880
+ />
881
+ </svg>
882
+ </button>
883
+ </div>
884
+
885
+ <div class="weekdays">
886
+ ${weekdayLabels.map(
887
+ (label) => html` <div class="weekday" aria-label="${label}">${label}</div> `
888
+ )}
889
+ </div>
890
+
891
+ <div class="calendar-grid" role="grid">
892
+ ${days.map(
893
+ (day, index) => html`
894
+ <button
895
+ class="day-button ${day.isSelected ? "selected" : ""} ${day.isToday
896
+ ? "today"
897
+ : ""} ${!day.isCurrentMonth ? "other-month" : ""}"
898
+ role="gridcell"
899
+ type="button"
900
+ aria-label="${this.formatISODate(day.date)}"
901
+ aria-selected="${day.isSelected}"
902
+ ?disabled=${day.isDisabled}
903
+ @click=${() => this.selectDay(day)}
904
+ @keydown=${(e: KeyboardEvent) => this.handleDayKeydown(e, day, index)}
905
+ tabindex="${day.isSelected || (index === 0 && !this.value) ? 0 : -1}"
906
+ >
907
+ ${day.day}
908
+ </button>
909
+ `
910
+ )}
911
+ </div>
912
+ </div>
913
+ </div>
914
+ `;
915
+ }
916
+ }