@reliability-design/web 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +43 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +18 -7
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +6 -6
- 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-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 +469 -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 +18 -10
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +20 -4
- 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-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer.ts +621 -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 +18 -10
- 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 +576 -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 +143 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +91 -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 +376 -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 +327 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +32 -5
- package/dist/components/rel-quote-page/README.md +166 -0
- package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +13 -5
- 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 +149 -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 +14 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +683 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +19 -7
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +6 -6
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +15 -6
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +36 -14
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +16 -0
- package/dist/components/rel-table/rel-table-caption.ts +22 -0
- package/dist/components/rel-table/rel-table-cell.ts +49 -0
- package/dist/components/rel-table/rel-table-footer.ts +18 -0
- package/dist/components/rel-table/rel-table-head.ts +33 -0
- package/dist/components/rel-table/rel-table-header.ts +16 -0
- package/dist/components/rel-table/rel-table-row.ts +32 -0
- package/dist/components/rel-table/rel-table.ts +47 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +35 -19
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +32 -14
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
- package/dist/index.d.mts +836 -1
- package/dist/index.d.ts +836 -1
- package/dist/index.js +10658 -1493
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10616 -1499
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +293 -183
- package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
- package/package.json +1 -1
|
@@ -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-colors-blue-primary);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.day-button.selected {
|
|
129
|
+
background: var(--rel-colors-green-primary);
|
|
130
|
+
color: var(--rel-colors-black);
|
|
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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# rel-card
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A simple, versatile card container component with built-in elevation shadow and hover lift effect. Provides a clean, elevated surface for grouping related content. Respects user motion preferences by disabling animations when requested.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
```html
|
|
8
|
+
<!-- Basic card with text content -->
|
|
9
|
+
<rel-card>
|
|
10
|
+
<h3>Card Title</h3>
|
|
11
|
+
<p>This is some content inside the card.</p>
|
|
12
|
+
</rel-card>
|
|
13
|
+
|
|
14
|
+
<!-- Card with mixed content -->
|
|
15
|
+
<rel-card>
|
|
16
|
+
<h2>Product Name</h2>
|
|
17
|
+
<img src="product.jpg" alt="Product" style="width: 100%; border-radius: 8px;">
|
|
18
|
+
<p>Product description goes here.</p>
|
|
19
|
+
<button>Add to Cart</button>
|
|
20
|
+
</rel-card>
|
|
21
|
+
|
|
22
|
+
<!-- Multiple cards in a grid -->
|
|
23
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;">
|
|
24
|
+
<rel-card>
|
|
25
|
+
<h4>Feature 1</h4>
|
|
26
|
+
<p>Description of feature 1</p>
|
|
27
|
+
</rel-card>
|
|
28
|
+
<rel-card>
|
|
29
|
+
<h4>Feature 2</h4>
|
|
30
|
+
<p>Description of feature 2</p>
|
|
31
|
+
</rel-card>
|
|
32
|
+
<rel-card>
|
|
33
|
+
<h4>Feature 3</h4>
|
|
34
|
+
<p>Description of feature 3</p>
|
|
35
|
+
</rel-card>
|
|
36
|
+
</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### Attributes/Properties
|
|
42
|
+
None. This component is purely presentational and controlled via slots.
|
|
43
|
+
|
|
44
|
+
## Events
|
|
45
|
+
None.
|
|
46
|
+
|
|
47
|
+
## Slots
|
|
48
|
+
| Slot | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| (default) | Card content (any HTML elements) |
|
|
51
|
+
|
|
52
|
+
## CSS Parts
|
|
53
|
+
None exposed.
|
|
54
|
+
|
|
55
|
+
## Tokens / CSS Variables
|
|
56
|
+
|
|
57
|
+
### Semantic Tokens
|
|
58
|
+
- `--rel-fontFamilies-primary` - Font family
|
|
59
|
+
|
|
60
|
+
### Color Tokens
|
|
61
|
+
- `--rel-colors-white` - Card background color
|
|
62
|
+
- `--rel-text-primary` - Text color
|
|
63
|
+
|
|
64
|
+
### Shadow Tokens
|
|
65
|
+
- `--rel-shadows-shadow-200` - Default elevation shadow
|
|
66
|
+
- `--rel-shadows-shadow-300` - Hover elevation shadow (larger)
|
|
67
|
+
|
|
68
|
+
### Border Tokens
|
|
69
|
+
- `--rel-borders-radius-border-radius-300` - Card border radius
|
|
70
|
+
|
|
71
|
+
### Spacing Tokens
|
|
72
|
+
- `--rel-spacing-space-md` - Internal padding
|
|
73
|
+
|
|
74
|
+
## Accessibility
|
|
75
|
+
- The card itself has no specific ARIA role (it's a generic container)
|
|
76
|
+
- Ensure slotted content includes proper semantic HTML and ARIA attributes where needed
|
|
77
|
+
- Hover effect is purely visual and does not affect accessibility
|
|
78
|
+
|
|
79
|
+
### Keyboard Interactions
|
|
80
|
+
- None (purely presentational container)
|
|
81
|
+
|
|
82
|
+
### Focus Behavior
|
|
83
|
+
- Does not receive focus (container only)
|
|
84
|
+
- Ensure slotted interactive elements are keyboard accessible
|
|
85
|
+
|
|
86
|
+
## Notes / Edge Cases
|
|
87
|
+
- The card is `display: block` and takes full width of its container by default
|
|
88
|
+
- **Hover effect**: Translates the card upward by 3px (`translateY(-3px)`) and increases shadow depth
|
|
89
|
+
- **Animation**: Uses `transition: transform 0.2s ease, box-shadow 0.2s ease`
|
|
90
|
+
- **Motion preferences**: When `prefers-reduced-motion: reduce`, both hover transform and transitions are disabled
|
|
91
|
+
- The card has consistent padding on all sides via `--rel-spacing-space-md`
|
|
92
|
+
- The internal `.card` wrapper is where all styling is applied
|
|
93
|
+
- The component provides no default content styling—you control the layout of slotted elements
|
|
94
|
+
- Works well with CSS Grid or Flexbox for creating card layouts
|
|
@@ -6,14 +6,14 @@ export class RelCard extends LitElement {
|
|
|
6
6
|
static styles = css`
|
|
7
7
|
:host {
|
|
8
8
|
display: block;
|
|
9
|
-
font-family: var(--rel-fontFamilies-primary);
|
|
9
|
+
font-family: var(--rel-fontFamilies-primary, sans-serif);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.card {
|
|
13
|
-
background-color: var(--rel-colors-white);
|
|
13
|
+
background-color: var(--rel-colors-white, #fff);
|
|
14
|
+
color: var(--rel-text-primary, #333);
|
|
14
15
|
border-radius: var(--rel-borders-radius-border-radius-300);
|
|
15
16
|
padding: var(--rel-spacing-space-md);
|
|
16
|
-
|
|
17
17
|
box-shadow: var(--rel-shadows-shadow-200);
|
|
18
18
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
19
19
|
}
|
|
@@ -22,6 +22,15 @@ export class RelCard extends LitElement {
|
|
|
22
22
|
transform: translateY(-3px);
|
|
23
23
|
box-shadow: var(--rel-shadows-shadow-300);
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-reduced-motion: reduce) {
|
|
27
|
+
.card {
|
|
28
|
+
transition: none;
|
|
29
|
+
}
|
|
30
|
+
.card:hover {
|
|
31
|
+
transform: none;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
25
34
|
`;
|
|
26
35
|
|
|
27
36
|
render() {
|