@crestbase/web-shared-ui 1.0.1 → 1.0.3
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/README.md +117 -4
- package/dist/components/button/CircleBtn.d.ts +9 -0
- package/dist/components/button/CircleBtn.js +5 -0
- package/dist/components/button/ShadowButton.d.ts +8 -0
- package/dist/components/button/ShadowButton.js +5 -0
- package/dist/components/button/index.d.ts +12 -3
- package/dist/components/button/index.js +17 -3
- package/dist/components/core/Accordion.d.ts +11 -0
- package/dist/components/core/Accordion.js +7 -0
- package/dist/components/core/AddressInput.d.ts +34 -0
- package/dist/components/core/AddressInput.js +88 -0
- package/dist/components/core/BottomSheet.d.ts +10 -0
- package/dist/components/core/BottomSheet.js +15 -0
- package/dist/components/core/Calendar.d.ts +17 -0
- package/dist/components/core/Calendar.js +440 -0
- package/dist/components/core/CalendarExtended.d.ts +36 -0
- package/dist/components/core/CalendarExtended.js +154 -0
- package/dist/components/core/Checkbox.d.ts +13 -0
- package/dist/components/core/Checkbox.js +10 -0
- package/dist/components/core/CustomDropdown.d.ts +19 -0
- package/dist/components/core/CustomDropdown.js +41 -0
- package/dist/components/core/Modal.d.ts +12 -0
- package/dist/components/core/Modal.js +12 -0
- package/dist/components/core/NumberInput.d.ts +12 -0
- package/dist/components/core/NumberInput.js +34 -0
- package/dist/components/core/NumberInputWithCommas.d.ts +10 -0
- package/dist/components/core/NumberInputWithCommas.js +19 -0
- package/dist/components/core/OptionsDropdown.d.ts +35 -0
- package/dist/components/core/OptionsDropdown.js +50 -0
- package/dist/components/core/OtpInput.d.ts +9 -0
- package/dist/components/core/OtpInput.js +54 -0
- package/dist/components/core/PreviewArea.d.ts +18 -0
- package/dist/components/core/PreviewArea.js +31 -0
- package/dist/components/core/SelectionList.d.ts +16 -0
- package/dist/components/core/SelectionList.js +9 -0
- package/dist/components/core/SquareCheckbox.d.ts +10 -0
- package/dist/components/core/SquareCheckbox.js +5 -0
- package/dist/components/core/TabNavigation.d.ts +19 -0
- package/dist/components/core/TabNavigation.js +11 -0
- package/dist/components/core/Textarea.d.ts +10 -0
- package/dist/components/core/Textarea.js +8 -0
- package/dist/components/core/Toggle.d.ts +10 -0
- package/dist/components/core/Toggle.js +11 -0
- package/dist/components/core/Upload.d.ts +28 -0
- package/dist/components/core/Upload.js +76 -0
- package/dist/components/core/UploadArea.d.ts +14 -0
- package/dist/components/core/UploadArea.js +13 -0
- package/dist/components/core/input.d.ts +11 -0
- package/dist/components/core/input.js +24 -0
- package/dist/components/index.d.ts +41 -1
- package/dist/components/index.js +25 -1
- package/dist/components/navigation/Logo.d.ts +7 -0
- package/dist/components/navigation/Logo.js +7 -0
- package/dist/components/notification/GlobalNotificationProvider.d.ts +4 -0
- package/dist/components/notification/GlobalNotificationProvider.js +6 -0
- package/dist/components/notification/NotificationPopup.d.ts +1 -0
- package/dist/components/notification/NotificationPopup.js +73 -0
- package/dist/components/notification/notificationStore.d.ts +15 -0
- package/dist/components/notification/notificationStore.js +12 -0
- package/dist/components/svg/Crestbase.d.ts +6 -0
- package/dist/components/svg/Crestbase.js +5 -0
- package/dist/components/svg/CrestbaseColored.d.ts +6 -0
- package/dist/components/svg/CrestbaseColored.js +5 -0
- package/dist/components/svg/UploadIcon.d.ts +5 -0
- package/dist/components/svg/UploadIcon.js +4 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useFileUpload.d.ts +35 -0
- package/dist/hooks/useFileUpload.js +219 -0
- package/dist/hooks/useFileUploadWithNotifications.d.ts +36 -0
- package/dist/hooks/useFileUploadWithNotifications.js +39 -0
- package/dist/hooks/useResponsive.d.ts +38 -0
- package/dist/hooks/useResponsive.js +97 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/styles.css +2 -0
- package/dist/types/property.d.ts +17 -0
- package/dist/types/property.js +2 -0
- package/dist/utils/fileValidation.d.ts +21 -0
- package/dist/utils/fileValidation.js +82 -0
- package/dist/utils/googleMapsLoader.d.ts +40 -0
- package/dist/utils/googleMapsLoader.js +108 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +13 -0
- package/package.json +22 -12
- package/dist/components/button/index.d.ts.map +0 -1
- package/dist/components/button/index.js.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState, useId, useCallback } from "react";
|
|
4
|
+
// Pure helper: parse a 24h time string "HH:MM" into minutes of day
|
|
5
|
+
const parseTimeString = (value) => {
|
|
6
|
+
const parts = value.split(":");
|
|
7
|
+
if (parts.length !== 2)
|
|
8
|
+
return null;
|
|
9
|
+
const hh = Number(parts[0]);
|
|
10
|
+
const mm = Number(parts[1]);
|
|
11
|
+
if (Number.isNaN(hh) || Number.isNaN(mm) || hh < 0 || hh > 23 || mm < 0 || mm > 59) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return hh * 60 + mm;
|
|
15
|
+
};
|
|
16
|
+
const Calendar = ({ selectedDates, onDateSelect, bookedDates = [], maxSelections, enableTimeSelection = false, allowPastDates = false, timeStepMinutes = 30, timeFormat = "12h", }) => {
|
|
17
|
+
const defaultPeriod = useMemo(() => (new Date().getHours() >= 12 ? "PM" : "AM"), []);
|
|
18
|
+
const [currentMonth, setCurrentMonth] = useState(new Date());
|
|
19
|
+
const [showTimePanel, setShowTimePanel] = useState(false);
|
|
20
|
+
const [tempDate, setTempDate] = useState(null);
|
|
21
|
+
// Redesigned time selection state
|
|
22
|
+
const [selectedHour, setSelectedHour] = useState(null);
|
|
23
|
+
const [selectedMinute, setSelectedMinute] = useState(null);
|
|
24
|
+
const [period, setPeriod] = useState(defaultPeriod);
|
|
25
|
+
const [editingPart, setEditingPart] = useState("hour");
|
|
26
|
+
const [minuteTens, setMinuteTens] = useState(null);
|
|
27
|
+
const [minuteOnes, setMinuteOnes] = useState(null);
|
|
28
|
+
// Derived minutes from current hour/minute/period selection
|
|
29
|
+
const minutesOfDayFromSelection = useMemo(() => {
|
|
30
|
+
if (selectedHour == null || selectedMinute == null)
|
|
31
|
+
return null;
|
|
32
|
+
const hours24 = timeFormat === "24h"
|
|
33
|
+
? selectedHour
|
|
34
|
+
: (selectedHour % 12) + (period === "PM" ? 12 : 0);
|
|
35
|
+
return hours24 * 60 + selectedMinute;
|
|
36
|
+
}, [selectedHour, selectedMinute, period, timeFormat]);
|
|
37
|
+
// Derived minutes even when only hour or only minute is selected (for preview)
|
|
38
|
+
const minutesOfDayFromPartialSelection = useMemo(() => {
|
|
39
|
+
if (selectedHour == null && selectedMinute == null)
|
|
40
|
+
return null;
|
|
41
|
+
const minutes = selectedMinute ?? 0;
|
|
42
|
+
const hours24 = timeFormat === "24h"
|
|
43
|
+
? selectedHour ?? 0
|
|
44
|
+
: ((selectedHour ?? 12) % 12) + (period === "PM" ? 12 : 0);
|
|
45
|
+
return hours24 * 60 + minutes;
|
|
46
|
+
}, [selectedHour, selectedMinute, period, timeFormat]);
|
|
47
|
+
const today = new Date();
|
|
48
|
+
const year = currentMonth.getFullYear();
|
|
49
|
+
const month = currentMonth.getMonth();
|
|
50
|
+
// Get first day of month and number of days
|
|
51
|
+
const firstDay = new Date(year, month, 1);
|
|
52
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
53
|
+
const daysInMonth = lastDay.getDate();
|
|
54
|
+
const startingDayOfWeek = firstDay.getDay();
|
|
55
|
+
// Month names
|
|
56
|
+
const monthNames = [
|
|
57
|
+
"January",
|
|
58
|
+
"February",
|
|
59
|
+
"March",
|
|
60
|
+
"April",
|
|
61
|
+
"May",
|
|
62
|
+
"June",
|
|
63
|
+
"July",
|
|
64
|
+
"August",
|
|
65
|
+
"September",
|
|
66
|
+
"October",
|
|
67
|
+
"November",
|
|
68
|
+
"December",
|
|
69
|
+
];
|
|
70
|
+
// Helper: date-only timestamp (moved earlier for predicate/useCallback ordering)
|
|
71
|
+
const toDateOnlyTs = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
72
|
+
// Helper: two-digit pad
|
|
73
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
74
|
+
// Helper: format minutes-of-day to label respecting 12h/24h
|
|
75
|
+
const formatTimeLabel = (minutesOfDay, fmt) => {
|
|
76
|
+
const hh = Math.floor(minutesOfDay / 60);
|
|
77
|
+
const mm = minutesOfDay % 60;
|
|
78
|
+
if (fmt === "24h") {
|
|
79
|
+
return `${pad2(hh)}:${pad2(mm)}`;
|
|
80
|
+
}
|
|
81
|
+
const periodLabel = hh >= 12 ? "PM" : "AM";
|
|
82
|
+
const h12 = hh % 12 === 0 ? 12 : hh % 12;
|
|
83
|
+
return `${h12}:${pad2(mm)} ${periodLabel}`;
|
|
84
|
+
};
|
|
85
|
+
// Predicates defined before usage to avoid TDZ/lint issues
|
|
86
|
+
const isDateSelected = useCallback((day) => {
|
|
87
|
+
const date = new Date(year, month, day);
|
|
88
|
+
const currentDateOnlyTs = toDateOnlyTs(date);
|
|
89
|
+
return selectedDates.some((selectedDate) => toDateOnlyTs(selectedDate) === currentDateOnlyTs);
|
|
90
|
+
}, [year, month, selectedDates]);
|
|
91
|
+
const isDateBooked = useCallback((day) => {
|
|
92
|
+
const date = new Date(year, month, day);
|
|
93
|
+
const currentDateOnlyTs = toDateOnlyTs(date);
|
|
94
|
+
return bookedDates.some((bookedDate) => toDateOnlyTs(bookedDate) === currentDateOnlyTs);
|
|
95
|
+
}, [year, month, bookedDates]);
|
|
96
|
+
const todayOnlyTs = useMemo(() => toDateOnlyTs(today), [today]);
|
|
97
|
+
const isDateDisabled = useCallback((day) => {
|
|
98
|
+
const date = new Date(year, month, day);
|
|
99
|
+
const dateOnlyTs = toDateOnlyTs(date);
|
|
100
|
+
return (!allowPastDates && dateOnlyTs < todayOnlyTs) || isDateBooked(day);
|
|
101
|
+
}, [year, month, todayOnlyTs, allowPastDates, isDateBooked]);
|
|
102
|
+
const handleDateClick = useCallback((day) => {
|
|
103
|
+
const clickedDate = new Date(year, month, day);
|
|
104
|
+
const clickedDateOnlyTs = toDateOnlyTs(clickedDate);
|
|
105
|
+
// Don't allow past dates or booked dates
|
|
106
|
+
if ((!allowPastDates && clickedDateOnlyTs < todayOnlyTs) || isDateBooked(day))
|
|
107
|
+
return;
|
|
108
|
+
// Check if we've reached max selections and this date isn't already selected
|
|
109
|
+
if (maxSelections &&
|
|
110
|
+
selectedDates.length >= maxSelections &&
|
|
111
|
+
!isDateSelected(day)) {
|
|
112
|
+
// When only one selection is allowed, permit switching days (replace later)
|
|
113
|
+
if (maxSelections !== 1) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!enableTimeSelection) {
|
|
118
|
+
onDateSelect(clickedDate);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Time selection flow
|
|
122
|
+
setTempDate(clickedDate);
|
|
123
|
+
setShowTimePanel(true);
|
|
124
|
+
// Prefill time if this day is already selected; otherwise default to 00 minutes
|
|
125
|
+
const existingForDay = selectedDates.find((selectedDate) => toDateOnlyTs(selectedDate) === clickedDateOnlyTs);
|
|
126
|
+
if (existingForDay) {
|
|
127
|
+
const hours24 = existingForDay.getHours();
|
|
128
|
+
const minutes = existingForDay.getMinutes();
|
|
129
|
+
if (timeFormat === "24h") {
|
|
130
|
+
setSelectedHour(hours24);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const newPeriod = hours24 >= 12 ? "PM" : "AM";
|
|
134
|
+
const hour12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
|
|
135
|
+
setPeriod(newPeriod);
|
|
136
|
+
setSelectedHour(hour12);
|
|
137
|
+
}
|
|
138
|
+
setSelectedMinute(minutes);
|
|
139
|
+
setMinuteTens(Math.floor(minutes / 10));
|
|
140
|
+
setMinuteOnes(minutes % 10);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// reset redesigned time selection state
|
|
144
|
+
setSelectedHour(null);
|
|
145
|
+
setSelectedMinute(0);
|
|
146
|
+
setMinuteTens(0);
|
|
147
|
+
setMinuteOnes(0);
|
|
148
|
+
setPeriod(defaultPeriod);
|
|
149
|
+
}
|
|
150
|
+
setEditingPart("hour");
|
|
151
|
+
}, [
|
|
152
|
+
year,
|
|
153
|
+
month,
|
|
154
|
+
todayOnlyTs,
|
|
155
|
+
allowPastDates,
|
|
156
|
+
isDateBooked,
|
|
157
|
+
maxSelections,
|
|
158
|
+
selectedDates,
|
|
159
|
+
isDateSelected,
|
|
160
|
+
enableTimeSelection,
|
|
161
|
+
onDateSelect,
|
|
162
|
+
timeFormat,
|
|
163
|
+
defaultPeriod,
|
|
164
|
+
]);
|
|
165
|
+
const handlePrevMonth = useCallback(() => {
|
|
166
|
+
setCurrentMonth(new Date(year, month - 1, 1));
|
|
167
|
+
}, [year, month]);
|
|
168
|
+
const handleNextMonth = useCallback(() => {
|
|
169
|
+
setCurrentMonth(new Date(year, month + 1, 1));
|
|
170
|
+
}, [year, month]);
|
|
171
|
+
const isToday = useMemo(() => {
|
|
172
|
+
if (!tempDate)
|
|
173
|
+
return false;
|
|
174
|
+
const d = tempDate;
|
|
175
|
+
return (d.getFullYear() === today.getFullYear() &&
|
|
176
|
+
d.getMonth() === today.getMonth() &&
|
|
177
|
+
d.getDate() === today.getDate());
|
|
178
|
+
}, [tempDate, today]);
|
|
179
|
+
// Accessibility: dynamic regions for date/time views
|
|
180
|
+
const dateViewHeadingId = useId();
|
|
181
|
+
const timeViewHeadingId = useId();
|
|
182
|
+
// Memoized hour options to avoid recreating arrays per render
|
|
183
|
+
const hourOptions24 = useMemo(() => Array.from({ length: 24 }, (_, i) => i), []);
|
|
184
|
+
const hourOptions12 = useMemo(() => Array.from({ length: 12 }, (_, i) => i + 1), []);
|
|
185
|
+
// Suggested quick-pick times, driven by timeStepMinutes while preserving defaults
|
|
186
|
+
const suggestedTimes = useMemo(() => {
|
|
187
|
+
const step = Math.max(1, timeStepMinutes ?? 30);
|
|
188
|
+
// Base anchors; for step=30 these remain 10:00, 12:00, 14:00
|
|
189
|
+
const anchors = [10 * 60, 12 * 60, 14 * 60];
|
|
190
|
+
return anchors.map((base) => {
|
|
191
|
+
const m = Math.floor(base / step) * step;
|
|
192
|
+
const label = formatTimeLabel(m, timeFormat);
|
|
193
|
+
let disabled = false;
|
|
194
|
+
if (isToday) {
|
|
195
|
+
const nowMinutes = today.getHours() * 60 + today.getMinutes();
|
|
196
|
+
disabled = m < nowMinutes;
|
|
197
|
+
}
|
|
198
|
+
return { label, minutes: m, disabled };
|
|
199
|
+
});
|
|
200
|
+
}, [timeStepMinutes, timeFormat, isToday, today]);
|
|
201
|
+
// Manual time entry (input[type="time"]) state
|
|
202
|
+
const [manualTime, setManualTime] = useState("");
|
|
203
|
+
// Sync manual time display when selection changes
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const m = minutesOfDayFromPartialSelection;
|
|
206
|
+
if (m == null)
|
|
207
|
+
return;
|
|
208
|
+
const hh = Math.floor(m / 60);
|
|
209
|
+
const mm = m % 60;
|
|
210
|
+
const value = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
|
|
211
|
+
setManualTime(value);
|
|
212
|
+
}, [/* sync with selection */ tempDate, minutesOfDayFromPartialSelection]);
|
|
213
|
+
const setSelectionFromMinutes = useCallback((minutesOfDay) => {
|
|
214
|
+
const hours24 = Math.floor(minutesOfDay / 60);
|
|
215
|
+
const minutes = minutesOfDay % 60;
|
|
216
|
+
if (timeFormat === "24h") {
|
|
217
|
+
setSelectedHour(hours24);
|
|
218
|
+
setSelectedMinute(minutes);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const newPeriod = hours24 >= 12 ? "PM" : "AM";
|
|
222
|
+
const hour12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
|
|
223
|
+
setPeriod(newPeriod);
|
|
224
|
+
setSelectedHour(hour12);
|
|
225
|
+
setSelectedMinute(minutes);
|
|
226
|
+
}
|
|
227
|
+
}, [timeFormat]);
|
|
228
|
+
const handleManualTimeChange = useCallback((value) => {
|
|
229
|
+
setManualTime(value);
|
|
230
|
+
const m = parseTimeString(value);
|
|
231
|
+
if (m != null) {
|
|
232
|
+
setSelectionFromMinutes(m);
|
|
233
|
+
}
|
|
234
|
+
}, [setSelectionFromMinutes]);
|
|
235
|
+
const handleTimeSelect = useCallback((minutesOfDay) => {
|
|
236
|
+
if (!tempDate)
|
|
237
|
+
return;
|
|
238
|
+
const d = new Date(tempDate);
|
|
239
|
+
const hours = Math.floor(minutesOfDay / 60);
|
|
240
|
+
const minutes = minutesOfDay % 60;
|
|
241
|
+
d.setHours(hours, minutes, 0, 0);
|
|
242
|
+
onDateSelect(d);
|
|
243
|
+
setShowTimePanel(false);
|
|
244
|
+
setTempDate(null);
|
|
245
|
+
// clear state
|
|
246
|
+
setSelectedHour(null);
|
|
247
|
+
setSelectedMinute(null);
|
|
248
|
+
setPeriod(defaultPeriod);
|
|
249
|
+
}, [tempDate, onDateSelect, defaultPeriod]);
|
|
250
|
+
// moved above to ensure availability for effects using it
|
|
251
|
+
const effectiveMinutesOfDay = useMemo(() => {
|
|
252
|
+
const manual = parseTimeString(manualTime);
|
|
253
|
+
return manual != null ? manual : minutesOfDayFromSelection;
|
|
254
|
+
}, [manualTime, minutesOfDayFromSelection]);
|
|
255
|
+
const isSelectedTimePast = useMemo(() => {
|
|
256
|
+
if (!isToday || effectiveMinutesOfDay == null)
|
|
257
|
+
return false;
|
|
258
|
+
const nowMinutes = today.getHours() * 60 + today.getMinutes();
|
|
259
|
+
return effectiveMinutesOfDay < nowMinutes;
|
|
260
|
+
}, [isToday, effectiveMinutesOfDay, today]);
|
|
261
|
+
// formatPreviewTime removed; use formatTimeLabel directly where needed
|
|
262
|
+
const handleConfirmSelection = useCallback(() => {
|
|
263
|
+
if (effectiveMinutesOfDay == null)
|
|
264
|
+
return;
|
|
265
|
+
if (isSelectedTimePast)
|
|
266
|
+
return;
|
|
267
|
+
handleTimeSelect(effectiveMinutesOfDay);
|
|
268
|
+
}, [effectiveMinutesOfDay, isSelectedTimePast, handleTimeSelect]);
|
|
269
|
+
// Generate calendar days (memoized)
|
|
270
|
+
const calendarDays = useMemo(() => {
|
|
271
|
+
const days = [];
|
|
272
|
+
// Add empty cells for days before the first day of the month
|
|
273
|
+
for (let i = 0; i < startingDayOfWeek; i++) {
|
|
274
|
+
days.push(_jsx("div", { className: "h-10" }, `empty-${i}`));
|
|
275
|
+
}
|
|
276
|
+
// Add days of the month
|
|
277
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
278
|
+
const isDisabled = isDateDisabled(day);
|
|
279
|
+
const isSelected = isDateSelected(day);
|
|
280
|
+
const isBooked = isDateBooked(day);
|
|
281
|
+
const dateOnly = new Date(year, month, day);
|
|
282
|
+
const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
283
|
+
const isPast = dateOnly < todayOnly;
|
|
284
|
+
const isCurrent = dateOnly.getTime() === todayOnly.getTime();
|
|
285
|
+
let buttonClass = "h-8 w-8 rounded-full flex items-center justify-center text-[11px] transition-all duration-200 ";
|
|
286
|
+
if (isDisabled) {
|
|
287
|
+
if (isBooked) {
|
|
288
|
+
buttonClass +=
|
|
289
|
+
"bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200";
|
|
290
|
+
}
|
|
291
|
+
else if (isPast) {
|
|
292
|
+
buttonClass += "text-gray-300 cursor-not-allowed";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (isSelected) {
|
|
296
|
+
buttonClass +=
|
|
297
|
+
"bg-blue-500 text-white shadow-md transform scale-105 cursor-pointer";
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
buttonClass +=
|
|
301
|
+
"text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer";
|
|
302
|
+
}
|
|
303
|
+
days.push(_jsx("button", { onClick: () => handleDateClick(day), disabled: isDisabled, className: buttonClass, "aria-selected": isSelected, "aria-current": isCurrent ? "date" : undefined, title: isBooked ? "This date is already booked" : isPast ? "Past date" : "", children: day }, day));
|
|
304
|
+
}
|
|
305
|
+
return days;
|
|
306
|
+
}, [
|
|
307
|
+
startingDayOfWeek,
|
|
308
|
+
daysInMonth,
|
|
309
|
+
year,
|
|
310
|
+
month,
|
|
311
|
+
today,
|
|
312
|
+
isDateDisabled,
|
|
313
|
+
isDateSelected,
|
|
314
|
+
isDateBooked,
|
|
315
|
+
handleDateClick,
|
|
316
|
+
]);
|
|
317
|
+
return (_jsxs("div", { className: "calendar ", children: [!(enableTimeSelection && showTimePanel && tempDate) && (_jsxs("div", { role: "region", "aria-labelledby": dateViewHeadingId, "aria-hidden": enableTimeSelection && showTimePanel && !!tempDate, className: "transition-all duration-300 ease-out", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("button", { onClick: handlePrevMonth, className: "p-2 rounded-full hover:bg-gray-100 transition-colors cursor-pointer", "aria-label": "Previous month", children: _jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsxs("h3", { id: dateViewHeadingId, className: "text-sm font-medium text-gray-800", children: [monthNames[month], " ", year] }), _jsx("button", { onClick: handleNextMonth, className: "p-2 rounded-full hover:bg-gray-100 transition-colors cursor-pointer", "aria-label": "Next month", children: _jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) }) })] }), _jsx("div", { className: "grid grid-cols-7 gap-1 mb-2", children: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (_jsx("div", { className: "h-8 flex items-center justify-center text-xs font-medium text-gray-500", children: day }, day))) }), _jsx("div", { className: "grid grid-cols-7 gap-1", children: calendarDays }), _jsx("div", { className: "mt-3", children: _jsxs("div", { className: "flex justify-center gap-4 text-xs", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-blue-500 rounded-full" }), _jsx("span", { className: "text-gray-600", children: "Selected" })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-3 h-3 bg-gray-100 border border-gray-200 rounded-full" }), _jsx("span", { className: "text-gray-600", children: "Booked" })] })] }) })] })), enableTimeSelection && showTimePanel && tempDate && (_jsx("div", { role: "region", "aria-labelledby": timeViewHeadingId, "aria-hidden": !(enableTimeSelection && showTimePanel && tempDate), className: "transition-all duration-300 ease-out", children: _jsxs("div", { className: "", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("button", { onClick: () => {
|
|
318
|
+
setShowTimePanel(false);
|
|
319
|
+
setTempDate(null);
|
|
320
|
+
setSelectedHour(null);
|
|
321
|
+
setSelectedMinute(null);
|
|
322
|
+
setPeriod(defaultPeriod);
|
|
323
|
+
}, className: "p-2 rounded-full hover:bg-gray-100 transition-colors cursor-pointer", "aria-label": "Back to date selection", type: "button", children: _jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsx("h4", { id: timeViewHeadingId, className: "text-sm font-medium text-gray-800", children: "Choose time" }), _jsx("span", { className: "text-xs text-gray-600", children: tempDate.toLocaleDateString(undefined, {
|
|
324
|
+
weekday: "short",
|
|
325
|
+
day: "numeric",
|
|
326
|
+
month: "short",
|
|
327
|
+
year: "numeric",
|
|
328
|
+
}) })] }), _jsxs("div", { className: "mb-3", children: [_jsx("span", { className: "text-xs text-gray-600 mr-2", children: "Quick picks:" }), _jsx("div", { className: "flex flex-wrap gap-2", role: "group", "aria-label": "Quick time suggestions", children: suggestedTimes.map((t) => (_jsx("button", { type: "button", "aria-disabled": t.disabled, disabled: t.disabled, onClick: () => handleTimeSelect(t.minutes), className: `text-[11px] px-3 py-1 rounded-full border transition-colors ${t.disabled
|
|
329
|
+
? "text-gray-400 bg-gray-100 cursor-not-allowed border-gray-200"
|
|
330
|
+
: "text-gray-700 hover:bg-blue-50 hover:text-blue-600 border-gray-300 cursor-pointer"}`, title: t.disabled ? "Unavailable in the past" : `Set ${t.label}`, children: t.label }, `suggest-${t.minutes}`))) })] }), timeFormat === "12h" && (_jsx("div", { className: "mb-3 flex items-center gap-2", role: "group", "aria-label": "AM/PM", children: ["AM", "PM"].map((p) => (_jsx("button", { type: "button", onClick: () => setPeriod(p), className: `text-[11px] px-3 py-1 rounded-full border ${period === p
|
|
331
|
+
? "bg-blue-50 text-blue-700 border-blue-300 cursor-pointer"
|
|
332
|
+
: "text-gray-700 hover:bg-blue-50 hover:text-blue-600 border-gray-300 cursor-pointer"}`, "aria-pressed": period === p, children: p }, p))) })), _jsx("div", { className: "mb-3", role: "group", "aria-label": editingPart === "hour" ? "Select hour" : "Select minute", children: _jsx("div", { className: "flex flex-wrap gap-2", children: (editingPart === "hour"
|
|
333
|
+
? timeFormat === "24h"
|
|
334
|
+
? hourOptions24
|
|
335
|
+
: hourOptions12
|
|
336
|
+
: [
|
|
337
|
+
{ label: "00", value: 0, type: "combo" },
|
|
338
|
+
{ label: "30", value: 30, type: "combo" },
|
|
339
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
340
|
+
label: String(i),
|
|
341
|
+
value: i,
|
|
342
|
+
type: "digit",
|
|
343
|
+
})),
|
|
344
|
+
]).map((item) => {
|
|
345
|
+
const val = typeof item === "number" ? item : item.value;
|
|
346
|
+
const isDigit = typeof item !== "number" && item.type === "digit";
|
|
347
|
+
const isCombo = typeof item !== "number" && item.type === "combo";
|
|
348
|
+
const isDisabled = editingPart === "minute" &&
|
|
349
|
+
isDigit &&
|
|
350
|
+
minuteTens == null &&
|
|
351
|
+
val > 5;
|
|
352
|
+
let btnClass = "h-8 w-8 rounded-full flex items-center justify-center text-[11px] transition-all duration-200 border ";
|
|
353
|
+
if (isDisabled) {
|
|
354
|
+
btnClass +=
|
|
355
|
+
"text-gray-400 bg-gray-100 cursor-not-allowed border-gray-200";
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
btnClass +=
|
|
359
|
+
"text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer border-gray-300";
|
|
360
|
+
}
|
|
361
|
+
return (_jsx("button", { type: "button", onClick: () => {
|
|
362
|
+
if (editingPart === "hour") {
|
|
363
|
+
setSelectedHour(val);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
if (isDisabled)
|
|
367
|
+
return;
|
|
368
|
+
if (isCombo) {
|
|
369
|
+
const minute = val;
|
|
370
|
+
setSelectedMinute(minute);
|
|
371
|
+
setMinuteTens(Math.floor(minute / 10));
|
|
372
|
+
setMinuteOnes(minute % 10);
|
|
373
|
+
setEditingPart("hour");
|
|
374
|
+
}
|
|
375
|
+
else if (isDigit) {
|
|
376
|
+
if (minuteTens == null ||
|
|
377
|
+
(minuteTens != null && minuteOnes != null)) {
|
|
378
|
+
setMinuteTens(val);
|
|
379
|
+
setMinuteOnes(null);
|
|
380
|
+
setSelectedMinute(null);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
setMinuteOnes(val);
|
|
384
|
+
const minute = (minuteTens ?? 0) * 10 + val;
|
|
385
|
+
setSelectedMinute(minute);
|
|
386
|
+
setEditingPart("hour");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}, className: btnClass, children: editingPart === "hour"
|
|
391
|
+
? String(val).padStart(2, "0")
|
|
392
|
+
: typeof item !== "number" &&
|
|
393
|
+
item.type === "combo"
|
|
394
|
+
? item.label
|
|
395
|
+
: String(val) }, `${editingPart}-${typeof item === "number" ? val : item.label}`));
|
|
396
|
+
}) }) }), _jsxs("div", { className: "flex items-center justify-between mt-4", children: [_jsxs("div", { className: "flex items-center justify-center gap-2 flex-1", children: [(() => {
|
|
397
|
+
const hourActive = editingPart === "hour";
|
|
398
|
+
const minuteActive = editingPart === "minute";
|
|
399
|
+
const hourVal = (() => {
|
|
400
|
+
if (selectedHour == null)
|
|
401
|
+
return "--";
|
|
402
|
+
return String(selectedHour).padStart(2, "0");
|
|
403
|
+
})();
|
|
404
|
+
const minuteVal = (() => {
|
|
405
|
+
if (selectedMinute != null)
|
|
406
|
+
return String(selectedMinute).padStart(2, "0");
|
|
407
|
+
if (minuteTens != null || minuteOnes != null) {
|
|
408
|
+
const tens = minuteTens != null ? String(minuteTens) : "-";
|
|
409
|
+
const ones = minuteOnes != null ? String(minuteOnes) : "-";
|
|
410
|
+
return `${tens}${ones}`;
|
|
411
|
+
}
|
|
412
|
+
return "--";
|
|
413
|
+
})();
|
|
414
|
+
const baseCls = "h-8 w-8 rounded-full flex items-center justify-center text-[11px] transition-all duration-200 border ";
|
|
415
|
+
const hourCls = baseCls +
|
|
416
|
+
(hourActive
|
|
417
|
+
? "bg-blue-500 text-white shadow-md transform scale-105 cursor-pointer border-blue-600"
|
|
418
|
+
: "text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer border-gray-300");
|
|
419
|
+
const minuteCls = baseCls +
|
|
420
|
+
(minuteActive
|
|
421
|
+
? "bg-blue-500 text-white shadow-md transform scale-105 cursor-pointer border-blue-600"
|
|
422
|
+
: "text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer border-gray-300");
|
|
423
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: hourCls, "aria-pressed": hourActive, onClick: () => setEditingPart("hour"), children: hourVal }), _jsx("span", { className: "text-gray-600", children: ":" }), _jsx("button", { type: "button", className: minuteCls, "aria-pressed": minuteActive, onClick: () => {
|
|
424
|
+
setEditingPart("minute");
|
|
425
|
+
setMinuteTens(null);
|
|
426
|
+
setMinuteOnes(null);
|
|
427
|
+
setSelectedMinute(null);
|
|
428
|
+
}, children: minuteVal })] }));
|
|
429
|
+
})(), isSelectedTimePast && (_jsx("span", { className: "text-[11px] text-red-600", children: "Time must be in the future" }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: handleConfirmSelection, disabled: effectiveMinutesOfDay == null || isSelectedTimePast, className: `text-[11px] px-3 py-2 rounded-xl border transition-colors ${effectiveMinutesOfDay == null || isSelectedTimePast
|
|
430
|
+
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
431
|
+
: "bg-blue-50 text-blue-700 hover:bg-blue-100 cursor-pointer"}`, "aria-label": "Confirm selected time", children: "Confirm" }), _jsx("button", { type: "button", onClick: () => {
|
|
432
|
+
setSelectedHour(null);
|
|
433
|
+
setSelectedMinute(null);
|
|
434
|
+
setPeriod(defaultPeriod);
|
|
435
|
+
setManualTime("");
|
|
436
|
+
setMinuteTens(null);
|
|
437
|
+
setMinuteOnes(null);
|
|
438
|
+
}, className: "text-[11px] px-3 py-2 rounded-xl border text-gray-700 hover:bg-gray-50 cursor-pointer", children: "Reset" })] })] })] }) }))] }));
|
|
439
|
+
};
|
|
440
|
+
export default Calendar;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type MonthSelection = {
|
|
2
|
+
year: number;
|
|
3
|
+
month: number;
|
|
4
|
+
};
|
|
5
|
+
export interface CalendarExtendedProps {
|
|
6
|
+
selectionGranularity?: "day" | "month";
|
|
7
|
+
allowRange?: boolean;
|
|
8
|
+
startYear?: number;
|
|
9
|
+
endYear?: number;
|
|
10
|
+
selectedMonths?: MonthSelection[];
|
|
11
|
+
onMonthSelectionChange?: (months: MonthSelection[]) => void;
|
|
12
|
+
onMonthRangeChange?: (range: {
|
|
13
|
+
start: MonthSelection;
|
|
14
|
+
end: MonthSelection;
|
|
15
|
+
}) => void;
|
|
16
|
+
/**
|
|
17
|
+
* When provided, highlights this range in the months grid.
|
|
18
|
+
* The component still manages internal clicks, but visual state
|
|
19
|
+
* uses this range preferentially if set.
|
|
20
|
+
*/
|
|
21
|
+
selectedRange?: {
|
|
22
|
+
start: MonthSelection;
|
|
23
|
+
end: MonthSelection;
|
|
24
|
+
};
|
|
25
|
+
minSelectableMonth?: MonthSelection;
|
|
26
|
+
maxSelectableMonth?: MonthSelection;
|
|
27
|
+
/** Convenience flag: when true, disallow selecting months beyond the current month */
|
|
28
|
+
disableFutureMonths?: boolean;
|
|
29
|
+
containerHeight?: number;
|
|
30
|
+
enableYearTransition?: boolean;
|
|
31
|
+
selectedDates?: Date[];
|
|
32
|
+
onDateSelect?: (date: Date) => void;
|
|
33
|
+
bookedDates?: Date[];
|
|
34
|
+
maxSelections?: number;
|
|
35
|
+
}
|
|
36
|
+
export default function CalendarExtended({ selectionGranularity, allowRange, startYear, endYear, selectedMonths, onMonthSelectionChange, onMonthRangeChange, selectedRange, minSelectableMonth, maxSelectableMonth, disableFutureMonths, containerHeight, enableYearTransition: _enableYearTransition, selectedDates, onDateSelect, bookedDates, maxSelections, }: Readonly<CalendarExtendedProps>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState, useRef } from "react";
|
|
4
|
+
import Calendar from "./Calendar";
|
|
5
|
+
const monthNames = [
|
|
6
|
+
"January",
|
|
7
|
+
"February",
|
|
8
|
+
"March",
|
|
9
|
+
"April",
|
|
10
|
+
"May",
|
|
11
|
+
"June",
|
|
12
|
+
"July",
|
|
13
|
+
"August",
|
|
14
|
+
"September",
|
|
15
|
+
"October",
|
|
16
|
+
"November",
|
|
17
|
+
"December",
|
|
18
|
+
];
|
|
19
|
+
export default function CalendarExtended({ selectionGranularity = "day", allowRange = false, startYear, endYear, selectedMonths = [], onMonthSelectionChange, onMonthRangeChange, selectedRange, minSelectableMonth, maxSelectableMonth, disableFutureMonths, containerHeight = 560, enableYearTransition: _enableYearTransition = true, selectedDates = [], onDateSelect, bookedDates, maxSelections, }) {
|
|
20
|
+
// Derive year bounds
|
|
21
|
+
const currentYear = new Date().getFullYear();
|
|
22
|
+
const DEFAULT_START_YEAR = 1900;
|
|
23
|
+
const DEFAULT_END_YEAR = disableFutureMonths ? currentYear : currentYear + 50;
|
|
24
|
+
const fromYear = startYear ?? minSelectableMonth?.year ?? DEFAULT_START_YEAR;
|
|
25
|
+
const toYear = endYear ?? maxSelectableMonth?.year ?? DEFAULT_END_YEAR;
|
|
26
|
+
// Internal range state if consumer enables range
|
|
27
|
+
const [rangeStart, setRangeStart] = useState(null);
|
|
28
|
+
const [rangeEnd, setRangeEnd] = useState(null);
|
|
29
|
+
// Effective visual range: prefer controlled selectedRange if provided
|
|
30
|
+
const effectiveRangeStart = selectedRange?.start ?? rangeStart ?? null;
|
|
31
|
+
const effectiveRangeEnd = selectedRange?.end ?? rangeEnd ?? null;
|
|
32
|
+
// Accessibility: grid id for aria-controls
|
|
33
|
+
const gridIdRef = useRef(`calendar-extended-grid-${Math.random().toString(36).slice(2)}`);
|
|
34
|
+
const allMonths = useMemo(() => {
|
|
35
|
+
const list = [];
|
|
36
|
+
for (let y = fromYear; y <= toYear; y++) {
|
|
37
|
+
for (let m = 0; m < 12; m++) {
|
|
38
|
+
list.push({ year: y, month: m });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return list;
|
|
42
|
+
}, [fromYear, toYear]);
|
|
43
|
+
const yearsList = useMemo(() => {
|
|
44
|
+
return Array.from({ length: toYear - fromYear + 1 }, (_, i) => fromYear + i);
|
|
45
|
+
}, [fromYear, toYear]);
|
|
46
|
+
const isSelected = (y, m) => selectedMonths.some((sel) => sel.year === y && sel.month === m);
|
|
47
|
+
const isInRange = (y, m) => {
|
|
48
|
+
if (!effectiveRangeStart || !effectiveRangeEnd)
|
|
49
|
+
return false;
|
|
50
|
+
const startKey = effectiveRangeStart.year * 12 + effectiveRangeStart.month;
|
|
51
|
+
const endKey = effectiveRangeEnd.year * 12 + effectiveRangeEnd.month;
|
|
52
|
+
const currentKey = y * 12 + m;
|
|
53
|
+
const [minKey, maxKey] = startKey <= endKey ? [startKey, endKey] : [endKey, startKey];
|
|
54
|
+
return currentKey >= minKey && currentKey <= maxKey;
|
|
55
|
+
};
|
|
56
|
+
const isMonthDisabled = (y, m) => {
|
|
57
|
+
const key = y * 12 + m;
|
|
58
|
+
const minKey = minSelectableMonth
|
|
59
|
+
? minSelectableMonth.year * 12 + minSelectableMonth.month
|
|
60
|
+
: null;
|
|
61
|
+
const maxKey = maxSelectableMonth
|
|
62
|
+
? maxSelectableMonth.year * 12 + maxSelectableMonth.month
|
|
63
|
+
: null;
|
|
64
|
+
const today = new Date();
|
|
65
|
+
const currentKey = today.getFullYear() * 12 + today.getMonth();
|
|
66
|
+
const futureDisabled = disableFutureMonths ? key > currentKey : false;
|
|
67
|
+
return ((minKey !== null && key < minKey) ||
|
|
68
|
+
(maxKey !== null && key > maxKey) ||
|
|
69
|
+
futureDisabled);
|
|
70
|
+
};
|
|
71
|
+
const handleMonthClick = (y, m) => {
|
|
72
|
+
const clicked = { year: y, month: m };
|
|
73
|
+
// Respect disabled constraints
|
|
74
|
+
if (isMonthDisabled(y, m)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (allowRange) {
|
|
78
|
+
if (!rangeStart) {
|
|
79
|
+
setRangeStart(clicked);
|
|
80
|
+
setRangeEnd(null);
|
|
81
|
+
onMonthRangeChange?.({ start: clicked, end: clicked });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// If start exists but end not set, set end
|
|
85
|
+
if (!rangeEnd) {
|
|
86
|
+
setRangeEnd(clicked);
|
|
87
|
+
onMonthRangeChange?.({ start: rangeStart, end: clicked });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Reset range on third click
|
|
91
|
+
setRangeStart(clicked);
|
|
92
|
+
setRangeEnd(null);
|
|
93
|
+
onMonthRangeChange?.({ start: clicked, end: clicked });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Single-month selection mode
|
|
97
|
+
const exists = isSelected(y, m);
|
|
98
|
+
let next = selectedMonths;
|
|
99
|
+
if (exists) {
|
|
100
|
+
next = selectedMonths.filter((sel) => !(sel.year === y && sel.month === m));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
next = [...selectedMonths, clicked];
|
|
104
|
+
}
|
|
105
|
+
onMonthSelectionChange?.(next);
|
|
106
|
+
};
|
|
107
|
+
// Show all years within computed bounds, even if all months are disabled
|
|
108
|
+
const visibleYears = yearsList;
|
|
109
|
+
const renderYearCard = (y) => {
|
|
110
|
+
const yearLabelId = `year-label-${y}-${gridIdRef.current}`;
|
|
111
|
+
return (_jsxs("div", { className: "p-4 transition-all duration-300 ease-out", "aria-labelledby": yearLabelId, children: [_jsx("div", { id: yearLabelId, className: "text-xs font-semibold mb-3", children: y }), _jsx("div", { role: "grid", "aria-label": `Months of ${y}`, className: "grid grid-cols-3 sm:grid-cols-4 gap-2", children: monthNames.map((name, m) => {
|
|
112
|
+
const selected = isSelected(y, m);
|
|
113
|
+
const inRange = isInRange(y, m);
|
|
114
|
+
const disabled = isMonthDisabled(y, m);
|
|
115
|
+
const isStart = !!effectiveRangeStart &&
|
|
116
|
+
effectiveRangeStart.year === y &&
|
|
117
|
+
effectiveRangeStart.month === m;
|
|
118
|
+
const isEnd = !!effectiveRangeEnd &&
|
|
119
|
+
effectiveRangeEnd.year === y &&
|
|
120
|
+
effectiveRangeEnd.month === m;
|
|
121
|
+
const isCurrent = (() => {
|
|
122
|
+
const d = new Date();
|
|
123
|
+
return d.getFullYear() === y && d.getMonth() === m;
|
|
124
|
+
})();
|
|
125
|
+
const isSinglePreRange = allowRange && !!rangeStart && !rangeEnd && isStart;
|
|
126
|
+
const baseClasses = "text-[10px] p-2 cursor-pointer rounded-lg transition-all duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-primaryblue/60";
|
|
127
|
+
const stateClasses = disabled
|
|
128
|
+
? "opacity-50 cursor-not-allowed bg-gray-50 text-gray-400"
|
|
129
|
+
: isStart && isEnd
|
|
130
|
+
? "bg-primaryblue text-white ring-2 ring-primaryblue"
|
|
131
|
+
: isStart
|
|
132
|
+
? "bg-primaryblue text-white ring-2 ring-primaryblue"
|
|
133
|
+
: isEnd
|
|
134
|
+
? "bg-primaryblue text-white ring-2 ring-primaryblue"
|
|
135
|
+
: inRange
|
|
136
|
+
? "bg-primaryblue/10 text-primaryblue ring-1 ring-primaryblue/30"
|
|
137
|
+
: isSinglePreRange || selected
|
|
138
|
+
? "bg-primaryblue/5 text-primaryblue"
|
|
139
|
+
: "bg-gray-100 hover:bg-gray-50";
|
|
140
|
+
const ariaSelected = selected || inRange || isStart || isEnd || isSinglePreRange;
|
|
141
|
+
return (_jsx("button", { type: "button", role: "gridcell", "aria-pressed": ariaSelected, "aria-disabled": disabled, "aria-selected": ariaSelected, "aria-current": isCurrent ? "date" : undefined, disabled: disabled, tabIndex: disabled ? -1 : 0, onClick: () => handleMonthClick(y, m), className: `${baseClasses} ${stateClasses}`, "aria-label": `${name} ${y}`, children: name }, `${y}-${m}`));
|
|
142
|
+
}) })] }, y));
|
|
143
|
+
};
|
|
144
|
+
if (selectionGranularity === "month") {
|
|
145
|
+
const containerStyle = {
|
|
146
|
+
maxHeight: containerHeight,
|
|
147
|
+
overflowY: "auto",
|
|
148
|
+
};
|
|
149
|
+
// Scroll mode only: show all years in a single-column, scrollable grid
|
|
150
|
+
return (_jsx("div", { id: gridIdRef.current, className: `grid grid-cols-1 md:grid-cols-1 xl:grid-cols-1`, style: containerStyle, role: "grid", "aria-label": "Years grid", children: visibleYears.map((y) => renderYearCard(y)) }));
|
|
151
|
+
}
|
|
152
|
+
// Default: day selection using existing Calendar (backward-compatible)
|
|
153
|
+
return (_jsx(Calendar, { selectedDates: selectedDates, onDateSelect: onDateSelect ?? (() => { }), bookedDates: bookedDates, maxSelections: maxSelections }));
|
|
154
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface CheckboxProps {
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
5
|
+
label?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const _default: (props: CheckboxProps) => React.JSX.Element;
|
|
13
|
+
export default _default;
|