@reliability-design/web 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +17 -32
  2. package/dist/components/rel-alert/rel-alert.ts +34 -34
  3. package/dist/components/rel-avatar/rel-avatar.ts +5 -3
  4. package/dist/components/rel-badge/rel-badge.ts +2 -1
  5. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +6 -6
  6. package/dist/components/rel-button/rel-button.ts +67 -21
  7. package/dist/components/rel-calendar/rel-calendar.ts +15 -8
  8. package/dist/components/rel-card/rel-card.ts +9 -17
  9. package/dist/components/rel-cardmedia/rel-cardmedia.ts +20 -23
  10. package/dist/components/rel-carousel/rel-carousel.ts +596 -468
  11. package/dist/components/rel-checkbox/rel-checkbox.ts +68 -16
  12. package/dist/components/rel-chip/rel-chip.ts +291 -244
  13. package/dist/components/rel-container/rel-container.ts +2 -2
  14. package/dist/components/rel-date-picker/rel-date-picker.ts +23 -19
  15. package/dist/components/rel-divider/rel-divider.ts +3 -3
  16. package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -2
  17. package/dist/components/rel-drawer/rel-drawer.ts +41 -88
  18. package/dist/components/rel-kbd/rel-kbd.ts +94 -118
  19. package/dist/components/rel-layout/rel-layout.ts +49 -58
  20. package/dist/components/rel-menu/rel-menu.ts +359 -359
  21. package/dist/components/rel-menubar/rel-menubar-menu.css +3 -0
  22. package/dist/components/rel-menubar/rel-menubar-menu.ts +2 -1
  23. package/dist/components/rel-menubar/rel-menubar.ts +602 -605
  24. package/dist/components/rel-otp/rel-otp.ts +22 -25
  25. package/dist/components/rel-pagination/rel-pagination.ts +101 -57
  26. package/dist/components/rel-popover/rel-popover-content.ts +6 -5
  27. package/dist/components/rel-popover/rel-popover.ts +26 -7
  28. package/dist/components/rel-progress/rel-progress.ts +10 -9
  29. package/dist/components/rel-radio/rel-radio.ts +228 -172
  30. package/dist/components/rel-range-picker/rel-range-picker.css +349 -0
  31. package/dist/components/rel-range-picker/rel-range-picker.ts +1079 -0
  32. package/dist/components/rel-resizable/rel-resizable-handle.ts +8 -7
  33. package/dist/components/rel-segmented-button/rel-segmented-button.ts +41 -20
  34. package/dist/components/rel-select/rel-select.ts +103 -32
  35. package/dist/components/rel-sheet/rel-sheet.ts +362 -684
  36. package/dist/components/rel-slider/rel-slider.ts +49 -27
  37. package/dist/components/rel-snackbar/rel-snackbar.ts +28 -28
  38. package/dist/components/rel-stepper/rel-stepper.ts +11 -10
  39. package/dist/components/rel-switch/rel-switch.ts +7 -6
  40. package/dist/components/rel-tab/rel-tab.ts +15 -29
  41. package/dist/components/rel-textfield/rel-textfield.ts +144 -35
  42. package/dist/components/rel-toast/rel-toast.ts +25 -12
  43. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -123
  44. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -0
  45. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -0
  46. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +32 -2
  47. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  48. package/dist/components/rel-topbar/rel-topbar-item.ts +15 -10
  49. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -0
  50. package/dist/components/rel-topbar/rel-topbar-profile.ts +30 -56
  51. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -0
  52. package/dist/components/rel-topbar/rel-topbar.ts +137 -28
  53. package/dist/index.d.mts +153 -178
  54. package/dist/index.d.ts +153 -178
  55. package/dist/index.js +4711 -3875
  56. package/dist/index.js.map +1 -1
  57. package/dist/index.mjs +4047 -3202
  58. package/dist/index.mjs.map +1 -1
  59. package/dist/rel-menubar-menu-BEEQDAXW.css +3 -0
  60. package/dist/styles/vars-v2.css +525 -0
  61. package/dist/styles/vars.css +346 -346
  62. package/dist/{vars-TBB4BMSD.css → vars-I2PC74XD.css} +346 -346
  63. package/dist/vars-v2-VMGWTFJZ.css +525 -0
  64. package/package.json +2 -1
@@ -0,0 +1,1079 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+ import "@reliability-design/icons/rel-icon";
4
+ import "../../styles/vars-v2.css";
5
+
6
+ interface RangeDay {
7
+ date: Date;
8
+ day: number;
9
+ isCurrentMonth: boolean;
10
+ isToday: boolean;
11
+ isDraftFrom: boolean;
12
+ isDraftTo: boolean;
13
+ isInRange: boolean;
14
+ isDisabled: boolean;
15
+ }
16
+
17
+ interface QuickSelect {
18
+ label: string;
19
+ key: string;
20
+ from: string;
21
+ to: string;
22
+ fullWidth?: boolean;
23
+ }
24
+
25
+ @customElement("rel-range-picker")
26
+ export class RelRangePicker extends LitElement {
27
+ @property({ type: String }) from: string = "";
28
+ @property({ type: String }) to: string = "";
29
+ @property({ type: String }) locale: string = "en-US";
30
+ @property({ type: String, attribute: "week-start" }) weekStart: "mon" | "sun" = "sun";
31
+ @property({ type: String, attribute: "min-date" }) minDate: string = "";
32
+ @property({ type: String, attribute: "max-date" }) maxDate: string = "";
33
+ @property({ type: Boolean, reflect: true }) disabled = false;
34
+ @property({ type: Boolean, reflect: true }) open = false;
35
+ @property({ type: String }) mode: "popover" | "popup" = "popover";
36
+
37
+ @state() private draftFrom: string = "";
38
+ @state() private draftTo: string = "";
39
+ @state() private draftActivePreset: string | null = null;
40
+ @state() private viewYear: number = new Date().getFullYear();
41
+ @state() private viewMonth: number = new Date().getMonth();
42
+ @state() private isSelectingRange: boolean = false;
43
+ @state() private hoverDate: string | null = null;
44
+
45
+ @query(".trigger-button") private triggerButton?: HTMLButtonElement;
46
+
47
+ static styles = css`
48
+ :host {
49
+ display: inline-block;
50
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
51
+ position: relative;
52
+ }
53
+
54
+ :host([disabled]) {
55
+ opacity: 0.6;
56
+ cursor: not-allowed;
57
+ pointer-events: none;
58
+ }
59
+
60
+ /* ── Trigger ── */
61
+ .trigger-button {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: space-between;
65
+ gap: var(--rel-spacing-space-xs, 8px);
66
+ padding: var(--rel-spacing-space-sm, 12px) var(--rel-spacing-space-md, 16px);
67
+ background: var(--rel-color-bg-primary, #fff);
68
+ border: 1px solid var(--rel-color-border-default, #ddd);
69
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
70
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
71
+ font-size: var(--rel-fontSizes-md, 14px);
72
+ color: var(--rel-color-text-primary, #333);
73
+ cursor: pointer;
74
+ min-width: 260px;
75
+ width: 100%;
76
+ text-align: left;
77
+ transition: border-color 0.15s ease;
78
+ }
79
+
80
+ .trigger-button:hover:not(:disabled) {
81
+ border-color: var(--rel-color-border-strong, #999);
82
+ }
83
+
84
+ .trigger-button:focus-visible {
85
+ outline: 2px solid var(--rel-color-border-focus, #0091ea);
86
+ outline-offset: 2px;
87
+ }
88
+
89
+ .trigger-content {
90
+ flex: 1;
91
+ color: var(--rel-color-text-primary, #333);
92
+ }
93
+
94
+ .trigger-content.placeholder {
95
+ color: var(--rel-color-text-placeholder, #a9a9a9);
96
+ }
97
+
98
+ /* ── Popover shell ── */
99
+ .popover {
100
+ position: absolute;
101
+ top: calc(100% + 4px);
102
+ left: 0;
103
+ z-index: 1000;
104
+ border: 1px solid var(--rel-color-border-default, #ddd);
105
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
106
+ background: var(--rel-color-bg-primary, #fff);
107
+ box-shadow: var(--rel-shadows-shadow-200);
108
+ overflow: visible;
109
+ display: none;
110
+ }
111
+
112
+ .popover.open { display: block; }
113
+
114
+ .popover.popup {
115
+ position: fixed;
116
+ top: 50%;
117
+ left: 50%;
118
+ transform: translate(-50%, -50%);
119
+ }
120
+
121
+ .backdrop {
122
+ position: fixed;
123
+ inset: 0;
124
+ z-index: 999;
125
+ background: rgba(0,0,0,0.4);
126
+ }
127
+
128
+ /* ── Two-panel wrapper ── */
129
+ .picker-wrapper {
130
+ display: flex;
131
+ flex-direction: row;
132
+ align-items: stretch;
133
+ }
134
+
135
+ /* ── Left panel: presets ── */
136
+ .presets {
137
+ width: 280px;
138
+ flex-shrink: 0;
139
+ flex-grow: 0;
140
+ border-right: 1px solid var(--rel-color-border-default, #ddd);
141
+ padding: 12px;
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 0;
145
+ }
146
+
147
+ .presets-grid {
148
+ display: grid;
149
+ grid-template-columns: 1fr 1fr;
150
+ gap: 6px;
151
+ }
152
+
153
+ .preset-item {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ padding: 10px 12px;
158
+ cursor: pointer;
159
+ font-size: 13px;
160
+ font-weight: var(--rel-fontWeights-medium, 500);
161
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
162
+ color: var(--rel-color-text-primary, #333);
163
+ background: var(--rel-color-bg-tertiary, #f5f5f5);
164
+ border: none;
165
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
166
+ width: 100%;
167
+ white-space: nowrap;
168
+ overflow: hidden;
169
+ text-overflow: ellipsis;
170
+ transition: background 0.15s ease;
171
+ line-height: 1.3;
172
+ }
173
+
174
+ .preset-item:hover:not(.active) {
175
+ background: var(--rel-color-bg-hover, #eeeeee);
176
+ }
177
+
178
+ .preset-item.active {
179
+ background: #1A1A1A;
180
+ color: #FFFFFF;
181
+ font-weight: var(--rel-fontWeights-semibold, 600);
182
+ }
183
+
184
+ .preset-item.full-width {
185
+ grid-column: 1 / -1;
186
+ }
187
+
188
+ /* ── FROM/TO inputs ── */
189
+ .date-inputs {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ padding: 14px 8px 4px;
194
+ border-top: 1px solid var(--rel-color-border-default, #ddd);
195
+ margin-top: auto;
196
+ }
197
+
198
+ .date-input-group {
199
+ flex: 1;
200
+ min-width: 0;
201
+ position: relative;
202
+ }
203
+
204
+ .date-input-label {
205
+ position: absolute;
206
+ top: -8px;
207
+ left: 10px;
208
+ font-size: 11px;
209
+ font-weight: var(--rel-fontWeights-medium, 500);
210
+ color: var(--rel-color-text-secondary, #9e9e9e);
211
+ background: var(--rel-color-bg-primary, #fff);
212
+ padding: 0 3px;
213
+ line-height: 1;
214
+ }
215
+
216
+ .date-input-value {
217
+ display: block;
218
+ border: 1px solid var(--rel-color-border-default, #ddd);
219
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
220
+ padding: 6px 8px;
221
+ font-size: var(--rel-fontSizes-md, 14px);
222
+ font-weight: var(--rel-fontWeights-semibold, 600);
223
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
224
+ color: var(--rel-color-text-primary, #333);
225
+ background: var(--rel-color-bg-primary, #fff);
226
+ width: 100%;
227
+ box-sizing: border-box;
228
+ }
229
+
230
+ .date-input-value.empty {
231
+ color: var(--rel-color-text-placeholder, #a9a9a9);
232
+ font-weight: var(--rel-fontWeights-regular, 400);
233
+ }
234
+
235
+ /* ── Right panel: calendar ── */
236
+ .calendar-panel {
237
+ width: 300px;
238
+ flex-shrink: 0;
239
+ flex-grow: 0;
240
+ padding: 12px;
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 6px;
244
+ }
245
+
246
+ .calendar-header {
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: space-between;
250
+ padding: 0 2px;
251
+ }
252
+
253
+ .month-label {
254
+ font-size: 15px;
255
+ font-weight: var(--rel-fontWeights-semibold, 600);
256
+ color: var(--rel-color-text-heading, #2b2b2b);
257
+ }
258
+
259
+ .nav-btn {
260
+ background: none;
261
+ border: none;
262
+ cursor: pointer;
263
+ color: var(--rel-color-text-primary, #333);
264
+ padding: 4px;
265
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
266
+ display: flex;
267
+ align-items: center;
268
+ transition: background 0.15s ease;
269
+ }
270
+
271
+ .nav-btn:hover { background: var(--rel-color-bg-hover, #f5f5f5); }
272
+
273
+ /* ── Weekday header ── */
274
+ .week-header {
275
+ display: grid;
276
+ grid-template-columns: repeat(7, 1fr);
277
+ text-align: center;
278
+ }
279
+
280
+ .week-day {
281
+ font-size: var(--rel-fontSizes-sm, 12px);
282
+ font-weight: var(--rel-fontWeights-semibold, 600);
283
+ color: var(--rel-color-text-secondary, #9e9e9e);
284
+ padding: 2px 0;
285
+ text-align: center;
286
+ }
287
+
288
+ /* ── Calendar grid ── */
289
+ .calendar-grid {
290
+ display: grid;
291
+ grid-template-columns: repeat(7, 1fr);
292
+ }
293
+
294
+ .day-cell {
295
+ height: 36px;
296
+ position: relative;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ cursor: pointer;
301
+ }
302
+
303
+ .day-cell.other-month {
304
+ opacity: 0.35;
305
+ pointer-events: none;
306
+ }
307
+
308
+ .day-cell.disabled {
309
+ opacity: 0.4;
310
+ pointer-events: none;
311
+ }
312
+
313
+ .day-cell.in-range {
314
+ background: var(--rel-color-bg-selected, #eeeeee);
315
+ }
316
+
317
+ .day-cell.range-start:not(.range-end) {
318
+ background: linear-gradient(to right, transparent 50%, var(--rel-color-bg-selected, #eeeeee) 50%);
319
+ }
320
+
321
+ .day-cell.range-end:not(.range-start) {
322
+ background: linear-gradient(to left, transparent 50%, var(--rel-color-bg-selected, #eeeeee) 50%);
323
+ }
324
+
325
+ .day-cell.range-start.range-end {
326
+ background: transparent;
327
+ }
328
+
329
+ /* ── Day inner circle ── */
330
+ .day-inner {
331
+ width: 32px;
332
+ height: 32px;
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ border-radius: 50%;
337
+ font-size: 13px;
338
+ font-weight: var(--rel-fontWeights-medium, 500);
339
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
340
+ color: var(--rel-color-text-primary, #333);
341
+ position: relative;
342
+ z-index: 1;
343
+ transition: background 0.15s ease;
344
+ line-height: 1;
345
+ }
346
+
347
+ .day-cell:not(.disabled):not(.other-month):not(.range-start):not(.range-end):hover .day-inner {
348
+ background: var(--rel-color-bg-hover, #f5f5f5);
349
+ }
350
+
351
+ /* Today: green outline */
352
+ .day-cell.today:not(.range-start):not(.range-end) .day-inner {
353
+ border: 2px solid var(--rel-colors-green-primary, #00f49c);
354
+ }
355
+
356
+ /* Range endpoints: dark fill */
357
+ .day-cell.range-start .day-inner,
358
+ .day-cell.range-end .day-inner {
359
+ background: #1A1A1A;
360
+ color: #FFFFFF;
361
+ font-weight: var(--rel-fontWeights-semibold, 600);
362
+ }
363
+
364
+ /* Today AND endpoint: green ring + dark fill */
365
+ .day-cell.today.range-start .day-inner,
366
+ .day-cell.today.range-end .day-inner {
367
+ background: #1A1A1A;
368
+ color: #FFFFFF;
369
+ border: 2px solid var(--rel-colors-green-primary, #00f49c);
370
+ }
371
+
372
+ /* ── Dark mode overrides ── */
373
+ :host-context([data-theme="dark"]) .day-cell.in-range,
374
+ :host-context(.dark) .day-cell.in-range {
375
+ background: var(--rel-color-bg-selected, #404040);
376
+ }
377
+
378
+ :host-context([data-theme="dark"]) .day-cell.range-start:not(.range-end),
379
+ :host-context(.dark) .day-cell.range-start:not(.range-end) {
380
+ background: linear-gradient(to right, transparent 50%, var(--rel-color-bg-selected, #404040) 50%);
381
+ }
382
+
383
+ :host-context([data-theme="dark"]) .day-cell.range-end:not(.range-start),
384
+ :host-context(.dark) .day-cell.range-end:not(.range-start) {
385
+ background: linear-gradient(to left, transparent 50%, var(--rel-color-bg-selected, #404040) 50%);
386
+ }
387
+
388
+ :host-context([data-theme="dark"]) .day-cell.range-start .day-inner,
389
+ :host-context([data-theme="dark"]) .day-cell.range-end .day-inner,
390
+ :host-context(.dark) .day-cell.range-start .day-inner,
391
+ :host-context(.dark) .day-cell.range-end .day-inner {
392
+ background: #F5F5F5;
393
+ color: #1A1A1A;
394
+ }
395
+
396
+ :host-context([data-theme="dark"]) .day-cell.today.range-start .day-inner,
397
+ :host-context([data-theme="dark"]) .day-cell.today.range-end .day-inner,
398
+ :host-context(.dark) .day-cell.today.range-start .day-inner,
399
+ :host-context(.dark) .day-cell.today.range-end .day-inner {
400
+ background: #F5F5F5;
401
+ color: #1A1A1A;
402
+ border: 2px solid var(--rel-colors-green-primary, #00f49c);
403
+ }
404
+
405
+ /* ── Footer ── */
406
+ .calendar-footer {
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
+ padding-top: 12px;
411
+ border-top: 1px solid var(--rel-color-border-default, #ddd);
412
+ margin-top: auto;
413
+ gap: 8px;
414
+ }
415
+
416
+ .btn-reset,
417
+ .btn-cancel,
418
+ .btn-apply {
419
+ font-family: var(--rel-fontFamilies-primary, Poppins, sans-serif);
420
+ font-size: var(--rel-fontSizes-md, 14px);
421
+ font-weight: var(--rel-fontWeights-medium, 500);
422
+ border-radius: var(--rel-borders-radius-border-radius-500, 1000px);
423
+ padding: 8px 20px;
424
+ cursor: pointer;
425
+ transition: background 0.15s ease, border-color 0.15s ease;
426
+ white-space: nowrap;
427
+ }
428
+
429
+ .btn-reset,
430
+ .btn-cancel {
431
+ background: transparent;
432
+ border: 1px solid var(--rel-color-border-default, #ddd);
433
+ color: var(--rel-color-text-primary, #333);
434
+ }
435
+
436
+ .btn-reset:hover:not(:disabled),
437
+ .btn-cancel:hover {
438
+ background: var(--rel-color-bg-hover, #f5f5f5);
439
+ }
440
+
441
+ .btn-reset:disabled {
442
+ opacity: 0.4;
443
+ cursor: not-allowed;
444
+ }
445
+
446
+ .btn-apply {
447
+ background: var(--rel-colors-green-primary, #00f49c);
448
+ border: none;
449
+ color: #000000;
450
+ }
451
+
452
+ .btn-apply:hover:not(:disabled) {
453
+ background: var(--rel-colors-green-secondary, #06d98c);
454
+ }
455
+
456
+ .btn-apply:disabled {
457
+ opacity: 0.4;
458
+ cursor: not-allowed;
459
+ }
460
+
461
+ .footer-right {
462
+ display: flex;
463
+ gap: 8px;
464
+ }
465
+
466
+ @media (prefers-reduced-motion: reduce) {
467
+ .preset-item, .nav-btn, .day-inner, .btn-reset, .btn-cancel, .btn-apply {
468
+ transition: none;
469
+ }
470
+ }
471
+ `;
472
+
473
+ // ── Lifecycle ──
474
+
475
+ connectedCallback() {
476
+ super.connectedCallback();
477
+ document.addEventListener("click", this.handleClickOutside);
478
+ document.addEventListener("keydown", this.handleGlobalKeydown);
479
+ }
480
+
481
+ disconnectedCallback() {
482
+ super.disconnectedCallback();
483
+ document.removeEventListener("click", this.handleClickOutside);
484
+ document.removeEventListener("keydown", this.handleGlobalKeydown);
485
+ }
486
+
487
+ // ── Open / close ──
488
+
489
+ private handleClickOutside = (e: MouseEvent) => {
490
+ if (!this.open) return;
491
+ const path = e.composedPath();
492
+ if (!path.includes(this)) {
493
+ this.cancelPopover();
494
+ }
495
+ };
496
+
497
+ private handleGlobalKeydown = (e: KeyboardEvent) => {
498
+ if (!this.open) return;
499
+
500
+ if (e.key === "Escape") {
501
+ this.cancelPopover();
502
+ this.triggerButton?.focus();
503
+ return;
504
+ }
505
+
506
+ if ((e.target as HTMLElement)?.tagName === "INPUT") return;
507
+
508
+ const key = e.key.toUpperCase();
509
+ const keyMap: Record<string, string> = {
510
+ T: "Today",
511
+ M: "Tomorrow",
512
+ B: "Before Today",
513
+ W: "This Week",
514
+ N: "Next Week",
515
+ "7": "Next 7 Days",
516
+ "3": "Next 30 Days",
517
+ };
518
+
519
+ const label = keyMap[key];
520
+ if (!label) return;
521
+
522
+ const qs = this.getQuickSelects().find((q) => q.label === label);
523
+ if (qs) this.selectQuickSelect(qs);
524
+ };
525
+
526
+ private toggleOpen() {
527
+ if (this.disabled) return;
528
+ if (this.open) {
529
+ this.cancelPopover();
530
+ } else {
531
+ this.openPopover();
532
+ }
533
+ }
534
+
535
+ private openPopover() {
536
+ this.draftFrom = this.from;
537
+ this.draftTo = this.to;
538
+ this.draftActivePreset = null;
539
+ this.isSelectingRange = false;
540
+ this.hoverDate = null;
541
+
542
+ if (this.draftFrom) {
543
+ const date = this.parseISODate(this.draftFrom);
544
+ if (date) {
545
+ this.viewYear = date.getFullYear();
546
+ this.viewMonth = date.getMonth();
547
+ }
548
+ } else {
549
+ const now = new Date();
550
+ this.viewYear = now.getFullYear();
551
+ this.viewMonth = now.getMonth();
552
+ }
553
+
554
+ this.open = true;
555
+ this.dispatchEvent(
556
+ new CustomEvent("rel-open-change", {
557
+ detail: { open: true },
558
+ bubbles: true,
559
+ composed: true,
560
+ })
561
+ );
562
+ }
563
+
564
+ private cancelPopover() {
565
+ if (!this.open) return;
566
+ this.open = false;
567
+ this.draftFrom = "";
568
+ this.draftTo = "";
569
+ this.draftActivePreset = null;
570
+ this.isSelectingRange = false;
571
+ this.hoverDate = null;
572
+ this.dispatchEvent(
573
+ new CustomEvent("rel-open-change", {
574
+ detail: { open: false },
575
+ bubbles: true,
576
+ composed: true,
577
+ })
578
+ );
579
+ }
580
+
581
+ private applySelection() {
582
+ if (this.draftFrom || this.draftTo) {
583
+ this.from = this.draftFrom;
584
+ this.to = this.draftTo;
585
+ this.dispatchEvent(
586
+ new CustomEvent("rel-change", {
587
+ detail: { from: this.from, to: this.to },
588
+ bubbles: true,
589
+ composed: true,
590
+ })
591
+ );
592
+ this.open = false;
593
+ this.isSelectingRange = false;
594
+ this.hoverDate = null;
595
+ this.dispatchEvent(
596
+ new CustomEvent("rel-open-change", {
597
+ detail: { open: false },
598
+ bubbles: true,
599
+ composed: true,
600
+ })
601
+ );
602
+ }
603
+ }
604
+
605
+ private resetDraft() {
606
+ if (this.draftFrom || this.draftTo) {
607
+ this.draftFrom = "";
608
+ this.draftTo = "";
609
+ this.draftActivePreset = null;
610
+ this.isSelectingRange = false;
611
+ this.hoverDate = null;
612
+ }
613
+ }
614
+
615
+ private getTriggerLabel(): string {
616
+ const from = this.from ? this.formatDisplayDate(this.from) : null;
617
+ const to = this.to ? this.formatDisplayDate(this.to) : null;
618
+ if (from && to) return `${from} → ${to}`;
619
+ if (from) return from;
620
+ return "";
621
+ }
622
+
623
+ // ── Date utilities ──
624
+
625
+ private parseISODate(iso: string): Date | null {
626
+ const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
627
+ if (!m) return null;
628
+ return new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
629
+ }
630
+
631
+ private formatISODate(date: Date): string {
632
+ const y = date.getFullYear();
633
+ const mo = String(date.getMonth() + 1).padStart(2, "0");
634
+ const d = String(date.getDate()).padStart(2, "0");
635
+ return `${y}-${mo}-${d}`;
636
+ }
637
+
638
+ private formatDisplayDate(iso: string): string {
639
+ const date = this.parseISODate(iso);
640
+ if (!date) return "";
641
+ return new Intl.DateTimeFormat("en-US", {
642
+ month: "2-digit",
643
+ day: "2-digit",
644
+ year: "numeric",
645
+ }).format(date);
646
+ }
647
+
648
+ private isSameDay(a: Date, b: Date): boolean {
649
+ return (
650
+ a.getFullYear() === b.getFullYear() &&
651
+ a.getMonth() === b.getMonth() &&
652
+ a.getDate() === b.getDate()
653
+ );
654
+ }
655
+
656
+ private isDateDisabled(date: Date): boolean {
657
+ if (this.minDate) {
658
+ const min = this.parseISODate(this.minDate);
659
+ if (min && date < min) return true;
660
+ }
661
+ if (this.maxDate) {
662
+ const max = this.parseISODate(this.maxDate);
663
+ if (max && date > max) return true;
664
+ }
665
+ return false;
666
+ }
667
+
668
+ // ── Quick selects ──
669
+
670
+ private getQuickSelects(): QuickSelect[] {
671
+ const today = new Date();
672
+ today.setHours(0, 0, 0, 0);
673
+
674
+ const yesterday = new Date(today);
675
+ yesterday.setDate(today.getDate() - 1);
676
+
677
+ const tomorrow = new Date(today);
678
+ tomorrow.setDate(today.getDate() + 1);
679
+
680
+ const dow = today.getDay(); // 0 = Sun
681
+ const thisSunday = new Date(today);
682
+ thisSunday.setDate(today.getDate() - dow);
683
+ const thisSaturday = new Date(thisSunday);
684
+ thisSaturday.setDate(thisSunday.getDate() + 6);
685
+
686
+ const nextSunday = new Date(thisSunday);
687
+ nextSunday.setDate(thisSunday.getDate() + 7);
688
+ const nextSaturday = new Date(nextSunday);
689
+ nextSaturday.setDate(nextSunday.getDate() + 6);
690
+
691
+ const next7To = new Date(today);
692
+ next7To.setDate(today.getDate() + 6);
693
+
694
+ const next30To = new Date(today);
695
+ next30To.setDate(today.getDate() + 29);
696
+
697
+ return [
698
+ { label: "Today", key: "T", from: this.formatISODate(today), to: this.formatISODate(today) },
699
+ { label: "Tomorrow", key: "M", from: this.formatISODate(tomorrow), to: this.formatISODate(tomorrow) },
700
+ { label: "Before Today", key: "B", from: "1970-01-01", to: this.formatISODate(yesterday) },
701
+ { label: "This Week", key: "W", from: this.formatISODate(thisSunday), to: this.formatISODate(thisSaturday) },
702
+ { label: "Next Week", key: "N", from: this.formatISODate(nextSunday), to: this.formatISODate(nextSaturday) },
703
+ { label: "Next 7 Days", key: "7", from: this.formatISODate(today), to: this.formatISODate(next7To) },
704
+ { label: "Next 30 Days", key: "3", from: this.formatISODate(today), to: this.formatISODate(next30To), fullWidth: true },
705
+ ];
706
+ }
707
+
708
+ private selectQuickSelect(qs: QuickSelect) {
709
+ if (this.disabled) return;
710
+ this.draftFrom = qs.from;
711
+ this.draftTo = qs.to;
712
+ this.draftActivePreset = qs.label;
713
+ this.isSelectingRange = false;
714
+ this.hoverDate = null;
715
+
716
+ const date = this.parseISODate(qs.from);
717
+ if (date) {
718
+ this.viewYear = date.getFullYear();
719
+ this.viewMonth = date.getMonth();
720
+ }
721
+ }
722
+
723
+ // ── Calendar navigation ──
724
+
725
+ private previousMonth() {
726
+ if (this.viewMonth === 0) {
727
+ this.viewMonth = 11;
728
+ this.viewYear--;
729
+ } else {
730
+ this.viewMonth--;
731
+ }
732
+ }
733
+
734
+ private nextMonth() {
735
+ if (this.viewMonth === 11) {
736
+ this.viewMonth = 0;
737
+ this.viewYear++;
738
+ } else {
739
+ this.viewMonth++;
740
+ }
741
+ }
742
+
743
+ private getMonthName(): string {
744
+ return new Intl.DateTimeFormat("en-US", { month: "long" }).format(
745
+ new Date(this.viewYear, this.viewMonth, 1)
746
+ );
747
+ }
748
+
749
+ private getWeekdayLabels(): string[] {
750
+ const formatter = new Intl.DateTimeFormat("en-US", { weekday: "short" });
751
+ const base = new Date(2024, 0, 7); // Sunday Jan 7 2024
752
+ return Array.from({ length: 7 }, (_, i) => {
753
+ const offset = this.weekStart === "mon" ? i + 1 : i;
754
+ const d = new Date(base);
755
+ d.setDate(base.getDate() + offset);
756
+ return formatter.format(d).substring(0, 2);
757
+ });
758
+ }
759
+
760
+ // ── Calendar day computation ──
761
+
762
+ private getCalendarDays(): RangeDay[] {
763
+ const today = new Date();
764
+ today.setHours(0, 0, 0, 0);
765
+
766
+ const firstDay = new Date(this.viewYear, this.viewMonth, 1);
767
+ const lastDay = new Date(this.viewYear, this.viewMonth + 1, 0);
768
+
769
+ const draftFromDate = this.draftFrom ? this.parseISODate(this.draftFrom) : null;
770
+ const draftToDate = this.draftTo ? this.parseISODate(this.draftTo) : null;
771
+
772
+ let effectiveFrom: Date | null = draftFromDate;
773
+ let effectiveTo: Date | null = draftToDate;
774
+
775
+ if (this.isSelectingRange && this.hoverDate && draftFromDate) {
776
+ const hDate = this.parseISODate(this.hoverDate);
777
+ if (hDate) {
778
+ if (hDate.getTime() >= draftFromDate.getTime()) {
779
+ effectiveFrom = draftFromDate;
780
+ effectiveTo = hDate;
781
+ } else {
782
+ effectiveFrom = hDate;
783
+ effectiveTo = draftFromDate;
784
+ }
785
+ }
786
+ }
787
+
788
+ const days: RangeDay[] = [];
789
+
790
+ let startDay = firstDay.getDay();
791
+ if (this.weekStart === "mon") {
792
+ startDay = startDay === 0 ? 6 : startDay - 1;
793
+ }
794
+ for (let i = startDay - 1; i >= 0; i--) {
795
+ const date = new Date(this.viewYear, this.viewMonth, -i);
796
+ days.push({
797
+ date,
798
+ day: date.getDate(),
799
+ isCurrentMonth: false,
800
+ isToday: false,
801
+ isDraftFrom: false,
802
+ isDraftTo: false,
803
+ isInRange: false,
804
+ isDisabled: true,
805
+ });
806
+ }
807
+
808
+ for (let d = 1; d <= lastDay.getDate(); d++) {
809
+ const date = new Date(this.viewYear, this.viewMonth, d);
810
+ const t = date.getTime();
811
+
812
+ const isDraftFrom = !!effectiveFrom && this.isSameDay(date, effectiveFrom);
813
+ const isDraftTo = !!effectiveTo && this.isSameDay(date, effectiveTo);
814
+ const isInRange =
815
+ !!effectiveFrom &&
816
+ !!effectiveTo &&
817
+ t > effectiveFrom.getTime() &&
818
+ t < effectiveTo.getTime();
819
+
820
+ days.push({
821
+ date,
822
+ day: d,
823
+ isCurrentMonth: true,
824
+ isToday: this.isSameDay(date, today),
825
+ isDraftFrom,
826
+ isDraftTo,
827
+ isInRange,
828
+ isDisabled: this.isDateDisabled(date),
829
+ });
830
+ }
831
+
832
+ const remaining = 42 - days.length;
833
+ for (let i = 1; i <= remaining; i++) {
834
+ const date = new Date(this.viewYear, this.viewMonth + 1, i);
835
+ days.push({
836
+ date,
837
+ day: date.getDate(),
838
+ isCurrentMonth: false,
839
+ isToday: false,
840
+ isDraftFrom: false,
841
+ isDraftTo: false,
842
+ isInRange: false,
843
+ isDisabled: true,
844
+ });
845
+ }
846
+
847
+ return days;
848
+ }
849
+
850
+ // ── Day click / hover ──
851
+
852
+ private handleDayClick(day: RangeDay) {
853
+ if (day.isDisabled || this.disabled) return;
854
+ const iso = this.formatISODate(day.date);
855
+
856
+ if (!this.isSelectingRange) {
857
+ this.draftFrom = iso;
858
+ this.draftTo = "";
859
+ this.draftActivePreset = null;
860
+ this.isSelectingRange = true;
861
+ this.hoverDate = null;
862
+ } else {
863
+ const fromDate = this.parseISODate(this.draftFrom);
864
+ if (!fromDate) {
865
+ this.draftFrom = iso;
866
+ this.draftTo = "";
867
+ return;
868
+ }
869
+
870
+ if (day.date.getTime() >= fromDate.getTime()) {
871
+ this.draftTo = iso;
872
+ } else {
873
+ this.draftTo = this.draftFrom;
874
+ this.draftFrom = iso;
875
+ }
876
+
877
+ this.isSelectingRange = false;
878
+ this.hoverDate = null;
879
+ this.draftActivePreset = null;
880
+ }
881
+ }
882
+
883
+ private handleDayHover(day: RangeDay) {
884
+ if (!this.isSelectingRange || day.isDisabled) return;
885
+ this.hoverDate = this.formatISODate(day.date);
886
+ }
887
+
888
+ // ── Render ──
889
+
890
+ render() {
891
+ const quickSelects = this.getQuickSelects();
892
+ const days = this.getCalendarDays();
893
+ const weekdayLabels = this.getWeekdayLabels();
894
+
895
+ const draftFromDisplay = this.draftFrom ? this.formatDisplayDate(this.draftFrom) : "";
896
+ const draftToDisplay = this.draftTo ? this.formatDisplayDate(this.draftTo) : "";
897
+ const triggerLabel = this.getTriggerLabel();
898
+ const isDraftEmpty = !this.draftFrom && !this.draftTo;
899
+ const isPopup = this.mode === "popup";
900
+
901
+ return html`
902
+ <button
903
+ class="trigger-button"
904
+ type="button"
905
+ ?disabled=${this.disabled}
906
+ aria-haspopup="dialog"
907
+ aria-expanded=${this.open}
908
+ aria-label=${triggerLabel || "Select date range"}
909
+ @click=${this.toggleOpen}
910
+ >
911
+ <span class="trigger-content ${triggerLabel ? "" : "placeholder"}">
912
+ ${triggerLabel || "Select date range"}
913
+ </span>
914
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
915
+ <path 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"/>
916
+ </svg>
917
+ </button>
918
+
919
+ ${isPopup && this.open
920
+ ? html`<div class="backdrop" @click=${this.cancelPopover}></div>`
921
+ : nothing}
922
+
923
+ <div
924
+ class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
925
+ role="dialog"
926
+ aria-label="Select date range"
927
+ >
928
+ <div class="picker-wrapper">
929
+
930
+ <!-- ── Left: Quick select ── -->
931
+ <div class="presets">
932
+ <div class="presets-grid" role="list" aria-label="Quick date selections">
933
+ ${quickSelects.map(
934
+ (qs) => html`
935
+ <button
936
+ class="preset-item ${qs.fullWidth ? "full-width" : ""} ${this.draftActivePreset === qs.label ? "active" : ""}"
937
+ type="button"
938
+ role="listitem"
939
+ ?disabled=${this.disabled}
940
+ aria-pressed=${this.draftActivePreset === qs.label}
941
+ @click=${() => this.selectQuickSelect(qs)}
942
+ >
943
+ <span>${qs.label}</span>
944
+ ${this.draftActivePreset === qs.label
945
+ ? html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
946
+ <circle cx="12" cy="12" r="12" fill="#00F49C"/>
947
+ <path d="M7 12.5l3.5 3.5 6.5-7" stroke="#000000" stroke-width="2"
948
+ stroke-linecap="round" stroke-linejoin="round"/>
949
+ </svg>`
950
+ : nothing}
951
+ </button>
952
+ `
953
+ )}
954
+ </div>
955
+
956
+ <div class="date-inputs">
957
+ <div class="date-input-group" aria-label="From date">
958
+ <span class="date-input-label">FROM</span>
959
+ <div class="date-input-value ${draftFromDisplay ? "" : "empty"}">
960
+ ${draftFromDisplay || "MM/DD/YYYY"}
961
+ </div>
962
+ </div>
963
+ <div class="date-input-group" aria-label="To date">
964
+ <span class="date-input-label">TO</span>
965
+ <div class="date-input-value ${draftToDisplay ? "" : "empty"}">
966
+ ${draftToDisplay || "MM/DD/YYYY"}
967
+ </div>
968
+ </div>
969
+ </div>
970
+ </div>
971
+
972
+ <!-- ── Right: Calendar panel ── -->
973
+ <div class="calendar-panel">
974
+
975
+ <div class="calendar-header">
976
+ <button
977
+ class="nav-btn"
978
+ type="button"
979
+ aria-label="Previous month"
980
+ @click=${this.previousMonth}
981
+ >
982
+ <rel-icon name="chevron-left" size="sm"></rel-icon>
983
+ </button>
984
+ <span class="month-label">${this.getMonthName()} ${this.viewYear}</span>
985
+ <button
986
+ class="nav-btn"
987
+ type="button"
988
+ aria-label="Next month"
989
+ @click=${this.nextMonth}
990
+ >
991
+ <rel-icon name="chevron-right" size="sm"></rel-icon>
992
+ </button>
993
+ </div>
994
+
995
+ <div class="week-header" role="row">
996
+ ${weekdayLabels.map(
997
+ (label) => html`
998
+ <div class="week-day" role="columnheader" aria-label="${label}">
999
+ ${label}
1000
+ </div>
1001
+ `
1002
+ )}
1003
+ </div>
1004
+
1005
+ <div
1006
+ class="calendar-grid"
1007
+ role="grid"
1008
+ aria-label="Select date range"
1009
+ @mouseleave=${() => { this.hoverDate = null; }}
1010
+ >
1011
+ ${days.map((day) => {
1012
+ const cellClasses = [
1013
+ "day-cell",
1014
+ day.isInRange ? "in-range" : "",
1015
+ day.isDraftFrom ? "range-start" : "",
1016
+ day.isDraftTo ? "range-end" : "",
1017
+ day.isToday ? "today" : "",
1018
+ day.isDisabled ? "disabled" : "",
1019
+ !day.isCurrentMonth ? "other-month" : "",
1020
+ ]
1021
+ .filter(Boolean)
1022
+ .join(" ");
1023
+
1024
+ return html`
1025
+ <div
1026
+ class="${cellClasses}"
1027
+ role="gridcell"
1028
+ aria-label="${this.formatISODate(day.date)}"
1029
+ aria-selected="${day.isDraftFrom || day.isDraftTo || day.isInRange}"
1030
+ @click=${() => this.handleDayClick(day)}
1031
+ @mouseenter=${() => this.handleDayHover(day)}
1032
+ >
1033
+ <span class="day-inner">${day.day}</span>
1034
+ </div>
1035
+ `;
1036
+ })}
1037
+ </div>
1038
+
1039
+ <div class="calendar-footer">
1040
+ <button
1041
+ class="btn-reset"
1042
+ type="button"
1043
+ ?disabled=${isDraftEmpty}
1044
+ @click=${this.resetDraft}
1045
+ >
1046
+ Reset
1047
+ </button>
1048
+ <div class="footer-right">
1049
+ <button
1050
+ class="btn-cancel"
1051
+ type="button"
1052
+ @click=${this.cancelPopover}
1053
+ >
1054
+ Cancel
1055
+ </button>
1056
+ <button
1057
+ class="btn-apply"
1058
+ type="button"
1059
+ ?disabled=${isDraftEmpty}
1060
+ @click=${this.applySelection}
1061
+ >
1062
+ Apply
1063
+ </button>
1064
+ </div>
1065
+ </div>
1066
+
1067
+ </div>
1068
+ </div>
1069
+ </div>
1070
+ `;
1071
+
1072
+ }
1073
+ }
1074
+
1075
+ declare global {
1076
+ interface HTMLElementTagNameMap {
1077
+ "rel-range-picker": RelRangePicker;
1078
+ }
1079
+ }