@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.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +47 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +101 -20
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-avatar/rel-avatar.ts +3 -2
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +11 -7
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
- package/dist/components/rel-button/README.md +130 -0
- package/dist/components/rel-button/rel-button.ts +64 -8
- package/dist/components/rel-calendar/README.md +109 -0
- package/dist/components/rel-calendar/index.ts +1 -0
- package/dist/components/rel-calendar/rel-calendar.ts +467 -0
- package/dist/components/rel-card/README.md +94 -0
- package/dist/components/rel-card/rel-card.ts +12 -3
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
- package/dist/components/rel-carousel/README.md +166 -0
- package/dist/components/rel-carousel/index.ts +1 -0
- package/dist/components/rel-carousel/rel-carousel.ts +468 -0
- package/dist/components/rel-chart/README.md +286 -0
- package/dist/components/rel-chart/index.ts +1 -0
- package/dist/components/rel-chart/rel-chart.ts +736 -0
- package/dist/components/rel-checkbox/README.md +77 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +51 -12
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +83 -9
- package/dist/components/rel-container/README.md +99 -0
- package/dist/components/rel-date-picker/README.md +253 -0
- package/dist/components/rel-date-picker/index.ts +1 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
- package/dist/components/rel-divider/README.md +100 -0
- package/dist/components/rel-divider/rel-divider.ts +9 -0
- package/dist/components/rel-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
- package/dist/components/rel-drawer/rel-drawer.ts +612 -0
- package/dist/components/rel-kbd/README.md +151 -0
- package/dist/components/rel-kbd/rel-kbd.ts +118 -0
- package/dist/components/rel-layout/README.md +88 -0
- package/dist/components/rel-menu/README.md +151 -0
- package/dist/components/rel-menu/rel-menu.ts +64 -24
- package/dist/components/rel-menubar/README.md +329 -0
- package/dist/components/rel-menubar/index.ts +2 -0
- package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
- package/dist/components/rel-menubar/rel-menubar.ts +605 -0
- package/dist/components/rel-nav-menu/README.md +289 -0
- package/dist/components/rel-nav-menu/index.ts +3 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +104 -0
- package/dist/components/rel-otp/README.md +440 -0
- package/dist/components/rel-otp/index.ts +1 -0
- package/dist/components/rel-otp/rel-otp.ts +504 -0
- package/dist/components/rel-pagination/README.md +410 -0
- package/dist/components/rel-pagination/index.ts +1 -0
- package/dist/components/rel-pagination/rel-pagination.ts +381 -0
- package/dist/components/rel-popover/README.md +568 -0
- package/dist/components/rel-popover/index.ts +2 -0
- package/dist/components/rel-popover/rel-popover-content.ts +55 -0
- package/dist/components/rel-popover/rel-popover.ts +330 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +32 -5
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +31 -6
- package/dist/components/rel-resizable/README.md +140 -0
- package/dist/components/rel-resizable/index.ts +3 -0
- package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
- package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
- package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
- package/dist/components/rel-segmented-button/README.md +141 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
- package/dist/components/rel-select/README.md +107 -0
- package/dist/components/rel-select/rel-select.ts +28 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +684 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +44 -9
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +38 -27
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +34 -12
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +79 -23
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +21 -0
- package/dist/components/rel-table/rel-table-caption.ts +27 -0
- package/dist/components/rel-table/rel-table-cell.ts +42 -0
- package/dist/components/rel-table/rel-table-footer.ts +23 -0
- package/dist/components/rel-table/rel-table-head.ts +45 -0
- package/dist/components/rel-table/rel-table-header.ts +21 -0
- package/dist/components/rel-table/rel-table-row.ts +36 -0
- package/dist/components/rel-table/rel-table.ts +53 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +55 -39
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +63 -24
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
- package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
- package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar.ts +329 -0
- package/dist/index.d.mts +831 -7
- package/dist/index.d.ts +831 -7
- package/dist/index.js +9500 -1564
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9240 -1344
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +187 -25
- package/dist/vars-TBB4BMSD.css +346 -0
- package/package.json +2 -1
- 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-
|
|
76
|
-
color: var(--rel-
|
|
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:
|
|
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:
|
|
112
|
+
color: #000000;
|
|
97
113
|
}
|
|
98
114
|
.btn-primary.tone-critical {
|
|
99
115
|
background: var(--rel-colors-red-primary);
|
|
100
|
-
color:
|
|
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-
|
|
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
|
-
|
|
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
|
+
}
|