@reliability-design/web 1.0.3 → 1.1.1

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 (126) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +47 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +101 -20
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +3 -2
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +11 -7
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +64 -8
  13. package/dist/components/rel-calendar/README.md +109 -0
  14. package/dist/components/rel-calendar/index.ts +1 -0
  15. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  16. package/dist/components/rel-card/README.md +94 -0
  17. package/dist/components/rel-card/rel-card.ts +12 -3
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  20. package/dist/components/rel-carousel/README.md +166 -0
  21. package/dist/components/rel-carousel/index.ts +1 -0
  22. package/dist/components/rel-carousel/rel-carousel.ts +468 -0
  23. package/dist/components/rel-chart/README.md +286 -0
  24. package/dist/components/rel-chart/index.ts +1 -0
  25. package/dist/components/rel-chart/rel-chart.ts +736 -0
  26. package/dist/components/rel-checkbox/README.md +77 -0
  27. package/dist/components/rel-checkbox/rel-checkbox.ts +51 -12
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +83 -9
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-date-picker/README.md +253 -0
  32. package/dist/components/rel-date-picker/index.ts +1 -0
  33. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  34. package/dist/components/rel-divider/README.md +100 -0
  35. package/dist/components/rel-divider/rel-divider.ts +9 -0
  36. package/dist/components/rel-drawer/README.md +200 -0
  37. package/dist/components/rel-drawer/index.ts +1 -0
  38. package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +612 -0
  40. package/dist/components/rel-kbd/README.md +151 -0
  41. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  42. package/dist/components/rel-layout/README.md +88 -0
  43. package/dist/components/rel-menu/README.md +151 -0
  44. package/dist/components/rel-menu/rel-menu.ts +64 -24
  45. package/dist/components/rel-menubar/README.md +329 -0
  46. package/dist/components/rel-menubar/index.ts +2 -0
  47. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  48. package/dist/components/rel-menubar/rel-menubar.ts +605 -0
  49. package/dist/components/rel-nav-menu/README.md +289 -0
  50. package/dist/components/rel-nav-menu/index.ts +3 -0
  51. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
  54. package/dist/components/rel-navbar/README.md +126 -0
  55. package/dist/components/rel-navbar/rel-navbar.ts +104 -0
  56. package/dist/components/rel-otp/README.md +440 -0
  57. package/dist/components/rel-otp/index.ts +1 -0
  58. package/dist/components/rel-otp/rel-otp.ts +504 -0
  59. package/dist/components/rel-pagination/README.md +410 -0
  60. package/dist/components/rel-pagination/index.ts +1 -0
  61. package/dist/components/rel-pagination/rel-pagination.ts +381 -0
  62. package/dist/components/rel-popover/README.md +568 -0
  63. package/dist/components/rel-popover/index.ts +2 -0
  64. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  65. package/dist/components/rel-popover/rel-popover.ts +330 -0
  66. package/dist/components/rel-progress/README.md +110 -0
  67. package/dist/components/rel-progress/rel-progress.ts +32 -5
  68. package/dist/components/rel-radio/README.md +120 -0
  69. package/dist/components/rel-radio/rel-radio.ts +31 -6
  70. package/dist/components/rel-resizable/README.md +140 -0
  71. package/dist/components/rel-resizable/index.ts +3 -0
  72. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  73. package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
  74. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  75. package/dist/components/rel-segmented-button/README.md +141 -0
  76. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  77. package/dist/components/rel-select/README.md +107 -0
  78. package/dist/components/rel-select/rel-select.ts +28 -14
  79. package/dist/components/rel-sheet/README.md +224 -0
  80. package/dist/components/rel-sheet/rel-sheet.ts +684 -0
  81. package/dist/components/rel-sidebar/README.md +209 -0
  82. package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
  83. package/dist/components/rel-slider/README.md +135 -0
  84. package/dist/components/rel-slider/rel-slider.ts +44 -9
  85. package/dist/components/rel-snackbar/README.md +134 -0
  86. package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
  87. package/dist/components/rel-stepper/README.md +160 -0
  88. package/dist/components/rel-stepper/rel-stepper.ts +38 -27
  89. package/dist/components/rel-switch/README.md +102 -0
  90. package/dist/components/rel-switch/rel-switch.ts +34 -12
  91. package/dist/components/rel-tab/README.md +163 -0
  92. package/dist/components/rel-tab/rel-tab.ts +79 -23
  93. package/dist/components/rel-table/README.md +178 -0
  94. package/dist/components/rel-table/rel-table-body.ts +21 -0
  95. package/dist/components/rel-table/rel-table-caption.ts +27 -0
  96. package/dist/components/rel-table/rel-table-cell.ts +42 -0
  97. package/dist/components/rel-table/rel-table-footer.ts +23 -0
  98. package/dist/components/rel-table/rel-table-head.ts +45 -0
  99. package/dist/components/rel-table/rel-table-header.ts +21 -0
  100. package/dist/components/rel-table/rel-table-row.ts +36 -0
  101. package/dist/components/rel-table/rel-table.ts +53 -0
  102. package/dist/components/rel-textfield/README.md +90 -0
  103. package/dist/components/rel-textfield/rel-textfield.ts +55 -39
  104. package/dist/components/rel-toast/README.md +148 -0
  105. package/dist/components/rel-toast/rel-toast.ts +63 -24
  106. package/dist/components/rel-tooltip/README.md +103 -0
  107. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
  108. package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
  109. package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
  110. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
  111. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  112. package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
  113. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
  114. package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
  115. package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
  116. package/dist/components/rel-topbar/rel-topbar.ts +329 -0
  117. package/dist/index.d.mts +831 -7
  118. package/dist/index.d.ts +831 -7
  119. package/dist/index.js +9500 -1564
  120. package/dist/index.js.map +1 -1
  121. package/dist/index.mjs +9240 -1344
  122. package/dist/index.mjs.map +1 -1
  123. package/dist/styles/vars.css +187 -25
  124. package/dist/vars-TBB4BMSD.css +346 -0
  125. package/package.json +2 -1
  126. package/dist/vars-6ZIUPIU4.css +0 -184
@@ -1,5 +1,6 @@
1
1
  import { LitElement, html, css } from "lit";
2
2
  import { customElement, property } from "lit/decorators.js";
3
+ import { ifDefined } from "lit/directives/if-defined.js";
3
4
 
4
5
  @customElement("rel-button")
5
6
  export class RelButton extends LitElement {
@@ -19,6 +20,8 @@ export class RelButton extends LitElement {
19
20
 
20
21
  @property({ type: Boolean, reflect: true }) disabled = false;
21
22
 
23
+ @property({ type: String, attribute: "aria-label" }) ariaLabel: string | null = null;
24
+
22
25
  static styles = css`
23
26
  :host {
24
27
  display: inline-flex;
@@ -70,11 +73,24 @@ export class RelButton extends LitElement {
70
73
  }
71
74
 
72
75
  /* Disabled */
76
+ :host([disabled]) {
77
+ cursor: not-allowed;
78
+ }
79
+
73
80
  :host([disabled]) button {
74
81
  cursor: not-allowed;
75
- background-color: var(--rel-colors-gray-border) !important;
76
- color: var(--rel-colors-gray-disabled-text) !important;
82
+ background-color: var(--rel-color-bg-tertiary) !important;
83
+ color: var(--rel-color-text-disabled) !important;
84
+ border: 1px solid var(--rel-color-border-default) !important;
77
85
  pointer-events: none;
86
+ filter: none !important;
87
+ }
88
+
89
+ :host([disabled]):host-context([data-theme="dark"]) button,
90
+ :host([disabled]):host-context(.dark) button {
91
+ background-color: var(--rel-color-bg-tertiary) !important;
92
+ color: var(--rel-color-text-disabled) !important;
93
+ border-color: var(--rel-color-border-default) !important;
78
94
  }
79
95
 
80
96
  /* Variants & tones (unchanged) */
@@ -84,7 +100,7 @@ export class RelButton extends LitElement {
84
100
  }
85
101
  .btn-default.tone-critical {
86
102
  background: var(--rel-colors-red-primary);
87
- color: var(--rel-colors-white);
103
+ color: #ffffff;
88
104
  }
89
105
  .btn-default.tone-success {
90
106
  background: var(--rel-colors-green-success);
@@ -93,11 +109,11 @@ export class RelButton extends LitElement {
93
109
 
94
110
  .btn-primary.tone-default {
95
111
  background: var(--rel-colors-green-primary);
96
- color: var(--rel-colors-black);
112
+ color: #000000;
97
113
  }
98
114
  .btn-primary.tone-critical {
99
115
  background: var(--rel-colors-red-primary);
100
- color: var(--rel-colors-white);
116
+ color: #ffffff;
101
117
  }
102
118
  .btn-primary.tone-success {
103
119
  background: var(--rel-colors-green-success);
@@ -106,7 +122,7 @@ export class RelButton extends LitElement {
106
122
 
107
123
  .btn-secondary {
108
124
  background: var(--rel-colors-white);
109
- border: 1px solid var(--rel-colors-black);
125
+ border: 1px solid var(--rel-color-border-strong);
110
126
  color: var(--rel-colors-black);
111
127
  }
112
128
 
@@ -130,8 +146,24 @@ export class RelButton extends LitElement {
130
146
  color: var(--rel-colors-black);
131
147
  }
132
148
 
133
- button:hover:not(:disabled) {
134
- filter: brightness(0.92);
149
+ button.btn-primary.tone-default:hover:not(:disabled) {
150
+ background: var(--rel-color-brand-hover);
151
+ }
152
+
153
+ button.btn-primary.tone-critical:hover:not(:disabled) {
154
+ filter: brightness(0.9);
155
+ }
156
+
157
+ button.btn-primary.tone-success:hover:not(:disabled) {
158
+ filter: brightness(0.9);
159
+ }
160
+
161
+ button:hover:not(:disabled):not(.btn-primary):not(.btn-default) {
162
+ background: var(--rel-color-bg-hover);
163
+ }
164
+
165
+ button.btn-default:hover:not(:disabled) {
166
+ filter: brightness(0.85);
135
167
  }
136
168
 
137
169
  button:active:not(:disabled) {
@@ -142,6 +174,29 @@ export class RelButton extends LitElement {
142
174
  outline: 2px solid var(--rel-colors-blue-primary);
143
175
  outline-offset: 2px;
144
176
  }
177
+
178
+ /* Dark mode overrides */
179
+ :host-context([data-theme="dark"]) button:focus-visible,
180
+ :host-context(.dark) button:focus-visible {
181
+ outline-color: #ffffff;
182
+ }
183
+
184
+ :host-context([data-theme="dark"]) button.btn-secondary,
185
+ :host-context(.dark) button.btn-secondary {
186
+ background: #000000;
187
+ border-color: #ffffff;
188
+ color: #ffffff;
189
+ }
190
+
191
+ :host-context([data-theme="dark"]) button.btn-text,
192
+ :host-context(.dark) button.btn-text {
193
+ color: #ffffff;
194
+ }
195
+
196
+ :host-context([data-theme="dark"]) button.btn-link,
197
+ :host-context(.dark) button.btn-link {
198
+ color: #ffffff;
199
+ }
145
200
  `;
146
201
 
147
202
  private get classes() {
@@ -176,6 +231,7 @@ export class RelButton extends LitElement {
176
231
  ?disabled=${this.disabled}
177
232
  @click=${this.handleClick}
178
233
  aria-disabled=${this.disabled}
234
+ aria-label=${ifDefined(this.ariaLabel ?? undefined)}
179
235
  >
180
236
  <slot></slot>
181
237
  </button>
@@ -0,0 +1,109 @@
1
+ # rel-calendar
2
+
3
+ A fully accessible calendar component built with Lit for the Reliability Design System.
4
+
5
+ ## Features
6
+
7
+ - Single date selection
8
+ - Month navigation
9
+ - Keyboard navigation support (arrow keys, Home/End, PageUp/PageDown)
10
+ - Min/max date constraints
11
+ - Custom disabled date predicate
12
+ - Configurable week start (Sunday or Monday)
13
+ - Locale-aware month names
14
+ - Today indicator
15
+ - Full ARIA support
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <rel-calendar></rel-calendar>
21
+ ```
22
+
23
+ ### With value
24
+
25
+ ```html
26
+ <rel-calendar value="2026-01-16"></rel-calendar>
27
+ ```
28
+
29
+ ### With min/max
30
+
31
+ ```html
32
+ <rel-calendar
33
+ min="2026-01-01"
34
+ max="2026-12-31"
35
+ ></rel-calendar>
36
+ ```
37
+
38
+ ### Week starting Monday
39
+
40
+ ```html
41
+ <rel-calendar week-start="mon"></rel-calendar>
42
+ ```
43
+
44
+ ### Disabled
45
+
46
+ ```html
47
+ <rel-calendar disabled></rel-calendar>
48
+ ```
49
+
50
+ ### Custom disabled dates (JavaScript)
51
+
52
+ ```javascript
53
+ const calendar = document.querySelector('rel-calendar');
54
+
55
+ // Disable weekends
56
+ calendar.isDateDisabled = (date) => {
57
+ const day = date.getDay();
58
+ return day === 0 || day === 6; // Sunday or Saturday
59
+ };
60
+
61
+ // Listen to changes
62
+ calendar.addEventListener('rel-change', (e) => {
63
+ console.log('Selected date:', e.detail.value); // "2026-01-16"
64
+ console.log('Selected date object:', e.detail.date); // Date object
65
+ });
66
+ ```
67
+
68
+ ## Attributes
69
+
70
+ | Attribute | Type | Default | Description |
71
+ |-----------|------|---------|-------------|
72
+ | `value` | `string \| null` | `null` | Selected date in ISO format (YYYY-MM-DD) |
73
+ | `min` | `string \| null` | `null` | Minimum selectable date (ISO format) |
74
+ | `max` | `string \| null` | `null` | Maximum selectable date (ISO format) |
75
+ | `disabled` | `boolean` | `false` | Disables all interaction |
76
+ | `week-start` | `"sun" \| "mon"` | `"sun"` | First day of the week |
77
+ | `locale` | `string` | `"en-US"` | Locale for month names (uses Intl.DateTimeFormat) |
78
+
79
+ ## Properties
80
+
81
+ | Property | Type | Description |
82
+ |----------|------|-------------|
83
+ | `isDateDisabled` | `(date: Date) => boolean` | Custom function to disable specific dates |
84
+
85
+ ## Events
86
+
87
+ | Event | Detail | Description |
88
+ |-------|--------|-------------|
89
+ | `rel-change` | `{ value: string \| null, date: Date \| null }` | Fired when selection changes |
90
+
91
+ ## Keyboard Navigation
92
+
93
+ - **Arrow keys**: Move focus by day (left/right) and week (up/down)
94
+ - **Home/End**: Jump to start/end of current week
95
+ - **PageUp/PageDown**: Change month
96
+ - **Shift + PageUp/PageDown**: Change year
97
+ - **Enter/Space**: Select focused day
98
+
99
+ ## Styling
100
+
101
+ The component uses design tokens from the Reliability Design System. All colors, spacing, and typography are inherited from CSS variables.
102
+
103
+ Key tokens used:
104
+ - `--rel-colors-*` for colors
105
+ - `--rel-spacing-*` for spacing
106
+ - `--rel-fontSizes-*`, `--rel-fontWeights-*` for typography
107
+ - `--rel-borders-radius-*` for border radius
108
+ - Focus outline uses `--rel-colors-blue-primary`
109
+ - Selected state uses `--rel-colors-green-primary`
@@ -0,0 +1 @@
1
+ export * from "./rel-calendar";
@@ -0,0 +1,467 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state } 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-calendar")
14
+ export class RelCalendar extends LitElement {
15
+ @property({ type: String, reflect: true }) value: string | null = null;
16
+ @property({ type: String }) min: string | null = null;
17
+ @property({ type: String }) max: string | null = null;
18
+ @property({ type: Boolean, reflect: true }) disabled = false;
19
+ @property({ type: String, attribute: "week-start" }) weekStart: "sun" | "mon" = "sun";
20
+ @property({ type: String }) locale = "en-US";
21
+
22
+ // Custom disabled predicate (not settable via attribute)
23
+ public isDateDisabled?: (date: Date) => boolean;
24
+
25
+ @state() private viewYear: number = new Date().getFullYear();
26
+ @state() private viewMonth: number = new Date().getMonth();
27
+ @state() private focusedDate: Date | null = null;
28
+
29
+ static styles = css`
30
+ :host {
31
+ display: block;
32
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
33
+ background: var(--rel-colors-white, #fff);
34
+ border: 1px solid var(--rel-border-default, #ddd);
35
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
36
+ padding: var(--rel-spacing-space-md, 16px);
37
+ max-width: 320px;
38
+ color: var(--rel-text-primary, #333);
39
+ }
40
+
41
+ :host([disabled]) {
42
+ opacity: 0.6;
43
+ pointer-events: none;
44
+ }
45
+
46
+ .header {
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ margin-bottom: var(--rel-spacing-space-md);
51
+ }
52
+
53
+ .header-center {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: var(--rel-spacing-space-xs);
57
+ font-size: var(--rel-fontSizes-md);
58
+ font-weight: var(--rel-fontWeights-semibold);
59
+ color: var(--rel-text-heading);
60
+ }
61
+
62
+ .nav-button {
63
+ background: transparent;
64
+ border: none;
65
+ width: 32px;
66
+ height: 32px;
67
+ border-radius: var(--rel-borders-radius-border-radius-200);
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ cursor: pointer;
72
+ color: var(--rel-colors-gray-primary);
73
+ transition: background-color 0.15s ease;
74
+ }
75
+
76
+ .nav-button:hover {
77
+ background: var(--rel-surface-secondary);
78
+ }
79
+
80
+ .nav-button:focus-visible {
81
+ outline: 2px solid var(--rel-colors-blue-primary);
82
+ outline-offset: 2px;
83
+ }
84
+
85
+ .calendar-grid {
86
+ display: grid;
87
+ grid-template-columns: repeat(7, 1fr);
88
+ gap: var(--rel-spacing-space-3xs);
89
+ }
90
+
91
+ .weekday {
92
+ text-align: center;
93
+ font-size: var(--rel-fontSizes-xs);
94
+ font-weight: var(--rel-fontWeights-semibold);
95
+ color: var(--rel-colors-gray-placeholder);
96
+ padding: var(--rel-spacing-space-xs);
97
+ text-transform: uppercase;
98
+ }
99
+
100
+ .day-button {
101
+ aspect-ratio: 1;
102
+ border: none;
103
+ background: transparent;
104
+ border-radius: var(--rel-borders-radius-border-radius-200);
105
+ font-size: var(--rel-fontSizes-sm);
106
+ font-weight: var(--rel-fontWeights-medium);
107
+ color: var(--rel-text-primary);
108
+ cursor: pointer;
109
+ position: relative;
110
+ transition: all 0.15s ease;
111
+ font-family: var(--rel-fontFamilies-primary);
112
+ }
113
+
114
+ .day-button:hover:not(:disabled):not(.selected) {
115
+ background: var(--rel-surface-secondary);
116
+ }
117
+
118
+ .day-button:focus-visible {
119
+ outline: 2px solid var(--rel-colors-blue-primary);
120
+ outline-offset: -2px;
121
+ z-index: 1;
122
+ }
123
+
124
+ .day-button.today:not(.selected) {
125
+ border: 2px solid var(--rel-color-border-focus);
126
+ }
127
+
128
+ .day-button.selected {
129
+ background: var(--rel-color-brand);
130
+ color: var(--rel-color-text-inverse);
131
+ font-weight: var(--rel-fontWeights-semibold);
132
+ }
133
+
134
+ .day-button.other-month {
135
+ color: var(--rel-colors-gray-placeholder);
136
+ opacity: 0.5;
137
+ }
138
+
139
+ .day-button:disabled {
140
+ color: var(--rel-text-disabled);
141
+ cursor: not-allowed;
142
+ opacity: 0.5;
143
+ }
144
+
145
+ .day-button:disabled:hover {
146
+ background: transparent;
147
+ }
148
+ `;
149
+
150
+ connectedCallback() {
151
+ super.connectedCallback();
152
+ if (this.value) {
153
+ const date = this.parseISODate(this.value);
154
+ if (date) {
155
+ this.viewYear = date.getFullYear();
156
+ this.viewMonth = date.getMonth();
157
+ }
158
+ }
159
+ }
160
+
161
+ updated(changedProperties: Map<string, any>) {
162
+ if (changedProperties.has("value") && this.value) {
163
+ const date = this.parseISODate(this.value);
164
+ if (date) {
165
+ this.viewYear = date.getFullYear();
166
+ this.viewMonth = date.getMonth();
167
+ }
168
+ }
169
+ }
170
+
171
+ private parseISODate(iso: string): Date | null {
172
+ const match = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
173
+ if (!match) return null;
174
+ const [, year, month, day] = match;
175
+ return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
176
+ }
177
+
178
+ private formatISODate(date: Date): string {
179
+ const year = date.getFullYear();
180
+ const month = String(date.getMonth() + 1).padStart(2, "0");
181
+ const day = String(date.getDate()).padStart(2, "0");
182
+ return `${year}-${month}-${day}`;
183
+ }
184
+
185
+ private isSameDay(date1: Date, date2: Date): boolean {
186
+ return (
187
+ date1.getFullYear() === date2.getFullYear() &&
188
+ date1.getMonth() === date2.getMonth() &&
189
+ date1.getDate() === date2.getDate()
190
+ );
191
+ }
192
+
193
+ private isDateDisabledInternal(date: Date): boolean {
194
+ if (this.disabled) return true;
195
+
196
+ if (this.min) {
197
+ const minDate = this.parseISODate(this.min);
198
+ if (minDate && date < minDate) return true;
199
+ }
200
+
201
+ if (this.max) {
202
+ const maxDate = this.parseISODate(this.max);
203
+ if (maxDate && date > maxDate) return true;
204
+ }
205
+
206
+ if (this.isDateDisabled && this.isDateDisabled(date)) {
207
+ return true;
208
+ }
209
+
210
+ return false;
211
+ }
212
+
213
+ private getCalendarDays(): CalendarDay[] {
214
+ const days: CalendarDay[] = [];
215
+ const firstDayOfMonth = new Date(this.viewYear, this.viewMonth, 1);
216
+ const lastDayOfMonth = new Date(this.viewYear, this.viewMonth + 1, 0);
217
+ const today = new Date();
218
+ const selectedDate = this.value ? this.parseISODate(this.value) : null;
219
+
220
+ let startDay = firstDayOfMonth.getDay();
221
+ if (this.weekStart === "mon") {
222
+ startDay = startDay === 0 ? 6 : startDay - 1;
223
+ }
224
+
225
+ // Leading days from previous month
226
+ for (let i = startDay - 1; i >= 0; i--) {
227
+ const date = new Date(this.viewYear, this.viewMonth, -i);
228
+ days.push({
229
+ date,
230
+ day: date.getDate(),
231
+ isCurrentMonth: false,
232
+ isToday: this.isSameDay(date, today),
233
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
234
+ isDisabled: this.isDateDisabledInternal(date),
235
+ });
236
+ }
237
+
238
+ // Current month days
239
+ for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
240
+ const date = new Date(this.viewYear, this.viewMonth, day);
241
+ days.push({
242
+ date,
243
+ day,
244
+ isCurrentMonth: true,
245
+ isToday: this.isSameDay(date, today),
246
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
247
+ isDisabled: this.isDateDisabledInternal(date),
248
+ });
249
+ }
250
+
251
+ // Trailing days from next month
252
+ const remainingCells = 42 - days.length; // 6 weeks max
253
+ for (let i = 1; i <= remainingCells; i++) {
254
+ const date = new Date(this.viewYear, this.viewMonth + 1, i);
255
+ days.push({
256
+ date,
257
+ day: date.getDate(),
258
+ isCurrentMonth: false,
259
+ isToday: this.isSameDay(date, today),
260
+ isSelected: selectedDate ? this.isSameDay(date, selectedDate) : false,
261
+ isDisabled: this.isDateDisabledInternal(date),
262
+ });
263
+ }
264
+
265
+ return days;
266
+ }
267
+
268
+ private getWeekdayLabels(): string[] {
269
+ const formatter = new Intl.DateTimeFormat(this.locale, { weekday: "short" });
270
+ const days = [];
271
+ const baseDate = new Date(2024, 0, 7); // Sunday, Jan 7, 2024
272
+
273
+ for (let i = 0; i < 7; i++) {
274
+ const offset = this.weekStart === "mon" ? i + 1 : i;
275
+ const date = new Date(baseDate);
276
+ date.setDate(baseDate.getDate() + offset);
277
+ days.push(formatter.format(date).substring(0, 2));
278
+ }
279
+
280
+ return days;
281
+ }
282
+
283
+ private getMonthName(): string {
284
+ const formatter = new Intl.DateTimeFormat(this.locale, { month: "long" });
285
+ return formatter.format(new Date(this.viewYear, this.viewMonth, 1));
286
+ }
287
+
288
+ private previousMonth() {
289
+ if (this.viewMonth === 0) {
290
+ this.viewMonth = 11;
291
+ this.viewYear--;
292
+ } else {
293
+ this.viewMonth--;
294
+ }
295
+ }
296
+
297
+ private nextMonth() {
298
+ if (this.viewMonth === 11) {
299
+ this.viewMonth = 0;
300
+ this.viewYear++;
301
+ } else {
302
+ this.viewMonth++;
303
+ }
304
+ }
305
+
306
+ private selectDay(day: CalendarDay) {
307
+ if (day.isDisabled || this.disabled) return;
308
+
309
+ const newValue = this.formatISODate(day.date);
310
+ this.value = newValue;
311
+ this.focusedDate = day.date;
312
+
313
+ this.dispatchEvent(
314
+ new CustomEvent("rel-change", {
315
+ detail: {
316
+ value: newValue,
317
+ date: new Date(day.date),
318
+ },
319
+ bubbles: true,
320
+ composed: true,
321
+ })
322
+ );
323
+ }
324
+
325
+ private handleKeyDown(e: KeyboardEvent, day: CalendarDay, index: number) {
326
+ const days = this.getCalendarDays();
327
+ let targetIndex = index;
328
+
329
+ switch (e.key) {
330
+ case "ArrowLeft":
331
+ e.preventDefault();
332
+ targetIndex = index - 1;
333
+ break;
334
+ case "ArrowRight":
335
+ e.preventDefault();
336
+ targetIndex = index + 1;
337
+ break;
338
+ case "ArrowUp":
339
+ e.preventDefault();
340
+ targetIndex = index - 7;
341
+ break;
342
+ case "ArrowDown":
343
+ e.preventDefault();
344
+ targetIndex = index + 7;
345
+ break;
346
+ case "Home":
347
+ e.preventDefault();
348
+ targetIndex = index - (index % 7);
349
+ break;
350
+ case "End":
351
+ e.preventDefault();
352
+ targetIndex = index - (index % 7) + 6;
353
+ break;
354
+ case "PageUp":
355
+ e.preventDefault();
356
+ if (e.shiftKey) {
357
+ this.viewYear--;
358
+ } else {
359
+ this.previousMonth();
360
+ }
361
+ return;
362
+ case "PageDown":
363
+ e.preventDefault();
364
+ if (e.shiftKey) {
365
+ this.viewYear++;
366
+ } else {
367
+ this.nextMonth();
368
+ }
369
+ return;
370
+ case "Enter":
371
+ case " ":
372
+ e.preventDefault();
373
+ this.selectDay(day);
374
+ return;
375
+ default:
376
+ return;
377
+ }
378
+
379
+ if (targetIndex >= 0 && targetIndex < days.length) {
380
+ const targetDay = days[targetIndex];
381
+ if (!targetDay.isCurrentMonth) {
382
+ if (targetIndex < index) {
383
+ this.previousMonth();
384
+ } else {
385
+ this.nextMonth();
386
+ }
387
+ }
388
+ this.focusedDate = targetDay.date;
389
+ this.requestUpdate();
390
+ this.updateComplete.then(() => {
391
+ const buttons = this.shadowRoot?.querySelectorAll(".day-button");
392
+ if (buttons && buttons[targetIndex]) {
393
+ (buttons[targetIndex] as HTMLButtonElement).focus();
394
+ }
395
+ });
396
+ }
397
+ }
398
+
399
+ render() {
400
+ const days = this.getCalendarDays();
401
+ const weekdayLabels = this.getWeekdayLabels();
402
+
403
+ return html`
404
+ <div role="grid" aria-label="Calendar">
405
+ <div class="header">
406
+ <button
407
+ class="nav-button"
408
+ @click=${this.previousMonth}
409
+ aria-label="Previous month"
410
+ ?disabled=${this.disabled}
411
+ >
412
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
413
+ <path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
414
+ </svg>
415
+ </button>
416
+
417
+ <div class="header-center">
418
+ <span>${this.getMonthName()}</span>
419
+ <span>${this.viewYear}</span>
420
+ </div>
421
+
422
+ <button
423
+ class="nav-button"
424
+ @click=${this.nextMonth}
425
+ aria-label="Next month"
426
+ ?disabled=${this.disabled}
427
+ >
428
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
429
+ <path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
430
+ </svg>
431
+ </button>
432
+ </div>
433
+
434
+ <div class="calendar-grid" role="row">
435
+ ${weekdayLabels.map(
436
+ (label) => html`
437
+ <div class="weekday" role="columnheader" aria-label="${label}">
438
+ ${label}
439
+ </div>
440
+ `
441
+ )}
442
+ </div>
443
+
444
+ <div class="calendar-grid">
445
+ ${days.map(
446
+ (day, index) => html`
447
+ <button
448
+ class="day-button ${day.isSelected ? "selected" : ""} ${day.isToday
449
+ ? "today"
450
+ : ""} ${!day.isCurrentMonth ? "other-month" : ""}"
451
+ role="gridcell"
452
+ aria-label="${this.formatISODate(day.date)}"
453
+ aria-selected="${day.isSelected}"
454
+ ?disabled=${day.isDisabled || (!day.isCurrentMonth && day.isDisabled)}
455
+ @click=${() => this.selectDay(day)}
456
+ @keydown=${(e: KeyboardEvent) => this.handleKeyDown(e, day, index)}
457
+ tabindex="${day.isSelected || (index === 0 && !this.focusedDate) ? 0 : -1}"
458
+ >
459
+ ${day.day}
460
+ </button>
461
+ `
462
+ )}
463
+ </div>
464
+ </div>
465
+ `;
466
+ }
467
+ }