@sonny-ui/core 0.1.0-alpha.14 → 0.1.0-alpha.16
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/fesm2022/sonny-ui-core.mjs +2257 -68
- package/fesm2022/sonny-ui-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/calendar/calendar.component.spec.ts +87 -0
- package/src/lib/calendar/calendar.component.ts +184 -61
- package/src/lib/calendar/calendar.types.ts +24 -0
- package/src/lib/calendar/index.ts +6 -0
- package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
- package/src/lib/color-picker/color-picker.component.ts +537 -0
- package/src/lib/color-picker/color-picker.types.ts +24 -0
- package/src/lib/color-picker/color-picker.utils.ts +183 -0
- package/src/lib/color-picker/color-picker.variants.ts +17 -0
- package/src/lib/color-picker/index.ts +20 -0
- package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
- package/src/lib/command-palette/command-palette.component.ts +195 -0
- package/src/lib/command-palette/command-palette.service.ts +36 -0
- package/src/lib/command-palette/command-palette.types.ts +23 -0
- package/src/lib/command-palette/index.ts +7 -0
- package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
- package/src/lib/date-picker/date-picker.component.ts +220 -0
- package/src/lib/date-picker/date-picker.variants.ts +17 -0
- package/src/lib/date-picker/index.ts +2 -0
- package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
- package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
- package/src/lib/date-range-picker/index.ts +1 -0
- package/src/lib/otp-input/index.ts +2 -0
- package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
- package/src/lib/otp-input/otp-input.component.ts +275 -0
- package/src/lib/otp-input/otp-input.variants.ts +18 -0
- package/types/sonny-ui-core.d.ts +331 -7
package/package.json
CHANGED
|
@@ -103,3 +103,90 @@ describe('SnyCalendarComponent — Reactive Forms', () => {
|
|
|
103
103
|
expect(allDisabled).toBe(true);
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
// --- Range Mode Tests ---
|
|
108
|
+
|
|
109
|
+
import type { DateRange } from './calendar.types';
|
|
110
|
+
|
|
111
|
+
@Component({
|
|
112
|
+
standalone: true,
|
|
113
|
+
imports: [SnyCalendarComponent],
|
|
114
|
+
template: `<sny-calendar mode="range" [(rangeValue)]="range" />`,
|
|
115
|
+
})
|
|
116
|
+
class RangeTestHost {
|
|
117
|
+
range = signal<DateRange | null>(null);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('SnyCalendarComponent — Range Mode', () => {
|
|
121
|
+
let fixture: ComponentFixture<RangeTestHost>;
|
|
122
|
+
let host: HTMLElement;
|
|
123
|
+
|
|
124
|
+
beforeEach(async () => {
|
|
125
|
+
await TestBed.configureTestingModule({ imports: [RangeTestHost] }).compileComponents();
|
|
126
|
+
fixture = TestBed.createComponent(RangeTestHost);
|
|
127
|
+
fixture.detectChanges();
|
|
128
|
+
host = fixture.nativeElement.querySelector('sny-calendar');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
function clickDay(dayNum: string): void {
|
|
132
|
+
const buttons = host.querySelectorAll('[role="grid"] button:not([disabled])');
|
|
133
|
+
const btn = Array.from(buttons).find((b) => b.textContent?.trim() === dayNum) as HTMLButtonElement;
|
|
134
|
+
btn?.click();
|
|
135
|
+
fixture.detectChanges();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it('should render 42 buttons in range mode', () => {
|
|
139
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
140
|
+
expect(buttons.length).toBe(42);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should set range start on first click', () => {
|
|
144
|
+
clickDay('10');
|
|
145
|
+
const range = fixture.componentInstance.range();
|
|
146
|
+
expect(range).not.toBeNull();
|
|
147
|
+
expect(range!.start).not.toBeNull();
|
|
148
|
+
expect(range!.start!.getDate()).toBe(10);
|
|
149
|
+
expect(range!.end).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should set range end on second click', () => {
|
|
153
|
+
clickDay('10');
|
|
154
|
+
clickDay('20');
|
|
155
|
+
const range = fixture.componentInstance.range();
|
|
156
|
+
expect(range!.start!.getDate()).toBe(10);
|
|
157
|
+
expect(range!.end!.getDate()).toBe(20);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should swap if second click is before start', () => {
|
|
161
|
+
clickDay('20');
|
|
162
|
+
clickDay('5');
|
|
163
|
+
const range = fixture.componentInstance.range();
|
|
164
|
+
expect(range!.start!.getDate()).toBe(5);
|
|
165
|
+
expect(range!.end!.getDate()).toBe(20);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should reset range on third click', () => {
|
|
169
|
+
clickDay('10');
|
|
170
|
+
clickDay('20');
|
|
171
|
+
clickDay('15');
|
|
172
|
+
const range = fixture.componentInstance.range();
|
|
173
|
+
expect(range!.start!.getDate()).toBe(15);
|
|
174
|
+
expect(range!.end).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should have range highlight classes when range is set', () => {
|
|
178
|
+
clickDay('10');
|
|
179
|
+
clickDay('15');
|
|
180
|
+
fixture.detectChanges();
|
|
181
|
+
const buttons = host.querySelectorAll('[role="grid"] button');
|
|
182
|
+
const classes = Array.from(buttons).map((b) => b.className);
|
|
183
|
+
const hasRangeStyle = classes.some((c) => c.includes('bg-primary/15') || c.includes('rounded-l-none') || c.includes('rounded-r-none'));
|
|
184
|
+
expect(hasRangeStyle).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should not affect single mode behavior', () => {
|
|
188
|
+
// This test uses the basic TestHostComponent which defaults to single mode
|
|
189
|
+
// Regression test: existing single mode tests above should still pass
|
|
190
|
+
expect(true).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -1,58 +1,70 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
forwardRef,
|
|
6
|
+
input,
|
|
7
|
+
linkedSignal,
|
|
8
|
+
model,
|
|
9
|
+
signal,
|
|
10
|
+
} from '@angular/core';
|
|
2
11
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
3
12
|
import { cn } from '../core/utils/cn';
|
|
4
|
-
|
|
5
|
-
interface CalendarDay {
|
|
6
|
-
date: Date;
|
|
7
|
-
day: number;
|
|
8
|
-
isCurrentMonth: boolean;
|
|
9
|
-
isToday: boolean;
|
|
10
|
-
isSelected: boolean;
|
|
11
|
-
isDisabled: boolean;
|
|
12
|
-
}
|
|
13
|
+
import type { CalendarDay, CalendarMode, DateRange } from './calendar.types';
|
|
13
14
|
|
|
14
15
|
@Component({
|
|
15
16
|
selector: 'sny-calendar',
|
|
16
17
|
standalone: true,
|
|
17
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
19
|
host: {
|
|
19
|
-
'[class]': '
|
|
20
|
+
'[class]': 'hostClass()',
|
|
20
21
|
'(keydown)': 'onKeydown($event)',
|
|
22
|
+
'role': 'application',
|
|
23
|
+
'aria-label': 'Calendar',
|
|
21
24
|
},
|
|
22
25
|
providers: [
|
|
23
26
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyCalendarComponent), multi: true },
|
|
24
27
|
],
|
|
25
28
|
template: `
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class="
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
@if (showNavigation()) {
|
|
30
|
+
<div class="flex items-center justify-between mb-3">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
34
|
+
(click)="prevMonth()"
|
|
35
|
+
aria-label="Previous month"
|
|
36
|
+
>
|
|
37
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
38
|
+
</button>
|
|
39
|
+
<span class="text-sm font-semibold tracking-tight">{{ monthYearLabel() }}</span>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="inline-flex items-center justify-center rounded-md h-8 w-8 hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
43
|
+
(click)="nextMonth()"
|
|
44
|
+
aria-label="Next month"
|
|
45
|
+
>
|
|
46
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
}
|
|
43
50
|
|
|
44
|
-
<div role="grid"
|
|
51
|
+
<div role="grid" class="grid grid-cols-7 gap-1">
|
|
45
52
|
@for (dayName of weekDays; track dayName) {
|
|
46
|
-
<div class="text-center text-xs text-muted-foreground font-medium
|
|
53
|
+
<div class="text-center text-xs text-muted-foreground font-medium h-9 flex items-center justify-center" role="columnheader">{{ dayName }}</div>
|
|
47
54
|
}
|
|
48
55
|
@for (day of days(); track day.date.getTime()) {
|
|
49
56
|
<button
|
|
57
|
+
type="button"
|
|
50
58
|
[class]="dayClass(day)"
|
|
51
59
|
[disabled]="day.isDisabled"
|
|
52
|
-
[attr.aria-selected]="day.isSelected || null"
|
|
60
|
+
[attr.aria-selected]="day.isSelected || day.isRangeStart || day.isRangeEnd || null"
|
|
53
61
|
[attr.aria-current]="day.isToday ? 'date' : null"
|
|
54
62
|
[attr.aria-disabled]="day.isDisabled || null"
|
|
55
|
-
|
|
63
|
+
[attr.aria-label]="day.date.toLocaleDateString(locale(), { month: 'long', day: 'numeric', year: 'numeric' })"
|
|
64
|
+
role="gridcell"
|
|
65
|
+
(click)="onDayClick(day.date)"
|
|
66
|
+
(mouseenter)="onDayHover(day.date)"
|
|
67
|
+
(mouseleave)="onDayHover(null)"
|
|
56
68
|
>
|
|
57
69
|
{{ day.day }}
|
|
58
70
|
</button>
|
|
@@ -61,28 +73,53 @@ interface CalendarDay {
|
|
|
61
73
|
`,
|
|
62
74
|
})
|
|
63
75
|
export class SnyCalendarComponent implements ControlValueAccessor {
|
|
76
|
+
// Existing inputs (backwards compatible)
|
|
64
77
|
readonly value = model<Date | null>(null);
|
|
65
78
|
readonly min = input<Date | undefined>(undefined);
|
|
66
79
|
readonly max = input<Date | undefined>(undefined);
|
|
67
80
|
readonly locale = input('en-US');
|
|
68
81
|
readonly class = input<string>('');
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
// Range mode inputs
|
|
84
|
+
readonly mode = input<CalendarMode>('single');
|
|
85
|
+
readonly rangeValue = model<DateRange | null>(null);
|
|
86
|
+
readonly showNavigation = input(true);
|
|
87
|
+
readonly initialViewDate = input<Date | undefined>(undefined);
|
|
88
|
+
readonly borderless = input(false);
|
|
71
89
|
|
|
72
|
-
readonly
|
|
90
|
+
readonly hostClass = computed(() =>
|
|
91
|
+
this.borderless()
|
|
92
|
+
? 'inline-block p-3 bg-background'
|
|
93
|
+
: 'inline-block p-4 rounded-md border border-border bg-background'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Internal state
|
|
97
|
+
private readonly _disabledByCva = signal(false);
|
|
98
|
+
readonly hoveredDate = signal<Date | null>(null);
|
|
99
|
+
readonly viewDate = linkedSignal(() => this.initialViewDate() ?? new Date());
|
|
73
100
|
readonly weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
74
101
|
|
|
75
|
-
|
|
102
|
+
// CVA
|
|
103
|
+
private _onChange: (value: unknown) => void = () => {};
|
|
76
104
|
protected onTouched: () => void = () => {};
|
|
77
105
|
|
|
78
|
-
writeValue(val:
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
writeValue(val: unknown): void {
|
|
107
|
+
if (this.mode() === 'range') {
|
|
108
|
+
this.rangeValue.set((val as DateRange) ?? null);
|
|
109
|
+
const range = val as DateRange | null;
|
|
110
|
+
if (range?.start) {
|
|
111
|
+
this.viewDate.set(new Date(range.start.getFullYear(), range.start.getMonth(), 1));
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
this.value.set((val as Date) ?? null);
|
|
115
|
+
if (val) {
|
|
116
|
+
const d = val as Date;
|
|
117
|
+
this.viewDate.set(new Date(d.getFullYear(), d.getMonth(), 1));
|
|
118
|
+
}
|
|
82
119
|
}
|
|
83
120
|
}
|
|
84
121
|
|
|
85
|
-
registerOnChange(fn: (value:
|
|
122
|
+
registerOnChange(fn: (value: unknown) => void): void {
|
|
86
123
|
this._onChange = fn;
|
|
87
124
|
}
|
|
88
125
|
|
|
@@ -94,6 +131,7 @@ export class SnyCalendarComponent implements ControlValueAccessor {
|
|
|
94
131
|
this._disabledByCva.set(isDisabled);
|
|
95
132
|
}
|
|
96
133
|
|
|
134
|
+
// Computed
|
|
97
135
|
readonly monthYearLabel = computed(() => {
|
|
98
136
|
const d = this.viewDate();
|
|
99
137
|
return d.toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' });
|
|
@@ -105,6 +143,8 @@ export class SnyCalendarComponent implements ControlValueAccessor {
|
|
|
105
143
|
const month = view.getMonth();
|
|
106
144
|
const today = new Date();
|
|
107
145
|
const selected = this.value();
|
|
146
|
+
const rangeVal = this.mode() === 'range' ? this.rangeValue() : null;
|
|
147
|
+
const hovered = this.hoveredDate();
|
|
108
148
|
const minDate = this.min();
|
|
109
149
|
const maxDate = this.max();
|
|
110
150
|
|
|
@@ -115,44 +155,78 @@ export class SnyCalendarComponent implements ControlValueAccessor {
|
|
|
115
155
|
|
|
116
156
|
const days: CalendarDay[] = [];
|
|
117
157
|
|
|
118
|
-
// Previous month
|
|
119
158
|
for (let i = startDay - 1; i >= 0; i--) {
|
|
120
159
|
const date = new Date(year, month - 1, daysInPrevMonth - i);
|
|
121
|
-
days.push(this.createDay(date, false, today, selected, minDate, maxDate));
|
|
160
|
+
days.push(this.createDay(date, false, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
122
161
|
}
|
|
123
162
|
|
|
124
|
-
// Current month
|
|
125
163
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
126
164
|
const date = new Date(year, month, d);
|
|
127
|
-
days.push(this.createDay(date, true, today, selected, minDate, maxDate));
|
|
165
|
+
days.push(this.createDay(date, true, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
128
166
|
}
|
|
129
167
|
|
|
130
|
-
// Next month fill
|
|
131
168
|
const remaining = 42 - days.length;
|
|
132
169
|
for (let d = 1; d <= remaining; d++) {
|
|
133
170
|
const date = new Date(year, month + 1, d);
|
|
134
|
-
days.push(this.createDay(date, false, today, selected, minDate, maxDate));
|
|
171
|
+
days.push(this.createDay(date, false, today, selected, rangeVal, hovered, minDate, maxDate));
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
return days;
|
|
138
175
|
});
|
|
139
176
|
|
|
177
|
+
// Navigation
|
|
140
178
|
prevMonth(): void {
|
|
141
|
-
this.viewDate.
|
|
179
|
+
this.viewDate.set(new Date(
|
|
180
|
+
this.viewDate().getFullYear(),
|
|
181
|
+
this.viewDate().getMonth() - 1,
|
|
182
|
+
1,
|
|
183
|
+
));
|
|
142
184
|
}
|
|
143
185
|
|
|
144
186
|
nextMonth(): void {
|
|
145
|
-
this.viewDate.
|
|
187
|
+
this.viewDate.set(new Date(
|
|
188
|
+
this.viewDate().getFullYear(),
|
|
189
|
+
this.viewDate().getMonth() + 1,
|
|
190
|
+
1,
|
|
191
|
+
));
|
|
146
192
|
}
|
|
147
193
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.
|
|
194
|
+
// Click handler
|
|
195
|
+
onDayClick(date: Date): void {
|
|
196
|
+
if (this.mode() === 'single') {
|
|
197
|
+
this.value.set(date);
|
|
198
|
+
this._onChange(date);
|
|
199
|
+
this.onTouched();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Range mode
|
|
204
|
+
const current = this.rangeValue();
|
|
205
|
+
if (!current?.start || (current.start && current.end)) {
|
|
206
|
+
this.rangeValue.set({ start: date, end: null });
|
|
207
|
+
} else {
|
|
208
|
+
const start = current.start;
|
|
209
|
+
if (date < start) {
|
|
210
|
+
this.rangeValue.set({ start: date, end: start });
|
|
211
|
+
} else if (this.isSameDay(date, start)) {
|
|
212
|
+
this.rangeValue.set({ start: date, end: date });
|
|
213
|
+
} else {
|
|
214
|
+
this.rangeValue.set({ start, end: date });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this._onChange(this.rangeValue());
|
|
151
218
|
this.onTouched();
|
|
152
219
|
}
|
|
153
220
|
|
|
221
|
+
// Hover handler
|
|
222
|
+
onDayHover(date: Date | null): void {
|
|
223
|
+
if (this.mode() === 'range') {
|
|
224
|
+
this.hoveredDate.set(date);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Keyboard
|
|
154
229
|
onKeydown(event: KeyboardEvent): void {
|
|
155
|
-
// Simplified keyboard navigation
|
|
156
230
|
switch (event.key) {
|
|
157
231
|
case 'ArrowLeft':
|
|
158
232
|
event.preventDefault();
|
|
@@ -173,17 +247,35 @@ export class SnyCalendarComponent implements ControlValueAccessor {
|
|
|
173
247
|
}
|
|
174
248
|
}
|
|
175
249
|
|
|
250
|
+
// Styling
|
|
176
251
|
dayClass(day: CalendarDay): string {
|
|
252
|
+
const isEndpoint = day.isRangeStart || day.isRangeEnd;
|
|
177
253
|
return cn(
|
|
178
|
-
'inline-flex items-center justify-center
|
|
179
|
-
|
|
180
|
-
day.
|
|
181
|
-
day.
|
|
182
|
-
day.
|
|
183
|
-
|
|
254
|
+
'inline-flex items-center justify-center text-sm h-9 w-9 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
255
|
+
// Shape
|
|
256
|
+
day.isRangeStart && !day.isRangeEnd ? 'rounded-l-md rounded-r-none' :
|
|
257
|
+
day.isRangeEnd && !day.isRangeStart ? 'rounded-r-md rounded-l-none' :
|
|
258
|
+
day.isInRange || day.isRangePreview ? 'rounded-none' :
|
|
259
|
+
'rounded-md',
|
|
260
|
+
// Base text color
|
|
261
|
+
!day.isCurrentMonth && 'text-muted-foreground/40',
|
|
262
|
+
day.isCurrentMonth && !day.isSelected && !isEndpoint && 'text-foreground',
|
|
263
|
+
// Today indicator
|
|
264
|
+
day.isToday && !day.isSelected && !isEndpoint && 'bg-accent text-accent-foreground font-semibold',
|
|
265
|
+
// Single selected
|
|
266
|
+
day.isSelected && this.mode() === 'single' && 'bg-primary text-primary-foreground font-semibold shadow-sm',
|
|
267
|
+
// Range endpoints
|
|
268
|
+
isEndpoint && 'bg-primary text-primary-foreground font-semibold shadow-sm',
|
|
269
|
+
// Range band
|
|
270
|
+
day.isInRange && 'bg-primary/10 text-foreground',
|
|
271
|
+
day.isRangePreview && 'bg-primary/5 text-foreground',
|
|
272
|
+
// States
|
|
273
|
+
day.isDisabled && 'opacity-40 cursor-not-allowed pointer-events-none',
|
|
274
|
+
!day.isDisabled && !day.isSelected && !isEndpoint && 'hover:bg-accent hover:text-accent-foreground cursor-pointer',
|
|
184
275
|
);
|
|
185
276
|
}
|
|
186
277
|
|
|
278
|
+
// Private helpers
|
|
187
279
|
private navigateDays(offset: number): void {
|
|
188
280
|
const current = this.value() ?? new Date();
|
|
189
281
|
const next = new Date(current);
|
|
@@ -198,16 +290,47 @@ export class SnyCalendarComponent implements ControlValueAccessor {
|
|
|
198
290
|
isCurrentMonth: boolean,
|
|
199
291
|
today: Date,
|
|
200
292
|
selected: Date | null,
|
|
293
|
+
rangeVal: DateRange | null,
|
|
294
|
+
hoveredDate: Date | null,
|
|
201
295
|
minDate: Date | undefined,
|
|
202
|
-
maxDate: Date | undefined
|
|
296
|
+
maxDate: Date | undefined,
|
|
203
297
|
): CalendarDay {
|
|
204
298
|
const isToday = this.isSameDay(date, today);
|
|
205
299
|
const isSelected = selected ? this.isSameDay(date, selected) : false;
|
|
206
300
|
const isDisabled =
|
|
207
301
|
this._disabledByCva() ||
|
|
208
|
-
(minDate ? date < minDate : false) ||
|
|
302
|
+
(minDate ? date < minDate : false) ||
|
|
303
|
+
(maxDate ? date > maxDate : false);
|
|
304
|
+
|
|
305
|
+
let isRangeStart = false;
|
|
306
|
+
let isRangeEnd = false;
|
|
307
|
+
let isInRange = false;
|
|
308
|
+
let isRangePreview = false;
|
|
309
|
+
|
|
310
|
+
if (rangeVal) {
|
|
311
|
+
const { start, end } = rangeVal;
|
|
312
|
+
if (start) isRangeStart = this.isSameDay(date, start);
|
|
313
|
+
if (end) isRangeEnd = this.isSameDay(date, end);
|
|
314
|
+
if (start && end) {
|
|
315
|
+
isInRange = date > start && date < end && !isRangeStart && !isRangeEnd;
|
|
316
|
+
}
|
|
317
|
+
// Preview: start set, no end yet, user hovering
|
|
318
|
+
if (start && !end && hoveredDate && !this.isSameDay(hoveredDate, start)) {
|
|
319
|
+
const previewStart = hoveredDate > start ? start : hoveredDate;
|
|
320
|
+
const previewEnd = hoveredDate > start ? hoveredDate : start;
|
|
321
|
+
if (date > previewStart && date < previewEnd) {
|
|
322
|
+
isRangePreview = true;
|
|
323
|
+
}
|
|
324
|
+
if (this.isSameDay(date, hoveredDate) && !isRangeStart) {
|
|
325
|
+
isRangePreview = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
209
329
|
|
|
210
|
-
return {
|
|
330
|
+
return {
|
|
331
|
+
date, day: date.getDate(), isCurrentMonth, isToday, isSelected, isDisabled,
|
|
332
|
+
isRangeStart, isRangeEnd, isInRange, isRangePreview,
|
|
333
|
+
};
|
|
211
334
|
}
|
|
212
335
|
|
|
213
336
|
private isSameDay(a: Date, b: Date): boolean {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DateRange {
|
|
2
|
+
start: Date | null;
|
|
3
|
+
end: Date | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CalendarDay {
|
|
7
|
+
date: Date;
|
|
8
|
+
day: number;
|
|
9
|
+
isCurrentMonth: boolean;
|
|
10
|
+
isToday: boolean;
|
|
11
|
+
isSelected: boolean;
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
isRangeStart: boolean;
|
|
14
|
+
isRangeEnd: boolean;
|
|
15
|
+
isInRange: boolean;
|
|
16
|
+
isRangePreview: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CalendarMode = 'single' | 'range';
|
|
20
|
+
|
|
21
|
+
export interface DatePickerPreset {
|
|
22
|
+
label: string;
|
|
23
|
+
range: DateRange;
|
|
24
|
+
}
|