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