@crestbase/web-shared-ui 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/components/core/Calendar.d.ts +8 -0
  2. package/dist/components/core/Calendar.js +397 -55
  3. package/dist/components/core/CalendarExtended.d.ts +10 -1
  4. package/dist/components/core/CalendarExtended.js +17 -10
  5. package/dist/components/core/CustomDropdown.js +2 -2
  6. package/dist/components/core/OptionsDropdown.d.ts +19 -1
  7. package/dist/components/core/OptionsDropdown.js +10 -4
  8. package/dist/components/core/PromptModal.d.ts +13 -0
  9. package/dist/components/core/PromptModal.js +20 -0
  10. package/dist/components/core/ShareModal.d.ts +8 -0
  11. package/dist/components/core/ShareModal.js +22 -0
  12. package/dist/components/index.d.ts +7 -0
  13. package/dist/components/index.js +8 -0
  14. package/dist/components/property-details/MediaItem.d.ts +13 -0
  15. package/dist/components/property-details/MediaItem.js +12 -0
  16. package/dist/components/property-details/MediaPreviewModal.d.ts +13 -0
  17. package/dist/components/property-details/MediaPreviewModal.js +24 -0
  18. package/dist/components/property-details/PropertyImageGallery.d.ts +7 -0
  19. package/dist/components/property-details/PropertyImageGallery.js +77 -0
  20. package/dist/components/property-details/mediaUtils.d.ts +11 -0
  21. package/dist/components/property-details/mediaUtils.js +64 -0
  22. package/dist/components/svg/ChevronIcon.d.ts +7 -0
  23. package/dist/components/svg/ChevronIcon.js +6 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/styles.css +1 -1
  27. package/dist/types/assets.d.ts +6 -0
  28. package/dist/types/assets.js +2 -0
  29. package/package.json +1 -1
@@ -4,6 +4,14 @@ interface CalendarProps {
4
4
  onDateSelect: (date: Date) => void;
5
5
  bookedDates?: Date[];
6
6
  maxSelections?: number;
7
+ /** Enable time selection after clicking a date */
8
+ enableTimeSelection?: boolean;
9
+ /** Allow selecting past dates (default false) */
10
+ allowPastDates?: boolean;
11
+ /** Minute step for available times, default 30 */
12
+ timeStepMinutes?: number;
13
+ /** Display format for time options */
14
+ timeFormat?: "12h" | "24h";
7
15
  }
8
16
  declare const _default: (props: CalendarProps) => React.JSX.Element;
9
17
  export default _default;
@@ -1,8 +1,49 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
4
- const Calendar = ({ selectedDates, onDateSelect, bookedDates = [], maxSelections, }) => {
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"), []);
5
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]);
6
47
  const today = new Date();
7
48
  const year = currentMonth.getFullYear();
8
49
  const month = currentMonth.getMonth();
@@ -26,73 +67,374 @@ const Calendar = ({ selectedDates, onDateSelect, bookedDates = [], maxSelections
26
67
  "November",
27
68
  "December",
28
69
  ];
29
- const handleDateClick = (day) => {
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) => {
30
103
  const clickedDate = new Date(year, month, day);
104
+ const clickedDateOnlyTs = toDateOnlyTs(clickedDate);
31
105
  // Don't allow past dates or booked dates
32
- if (clickedDate < today || isDateBooked(day))
106
+ if ((!allowPastDates && clickedDateOnlyTs < todayOnlyTs) || isDateBooked(day))
33
107
  return;
34
108
  // Check if we've reached max selections and this date isn't already selected
35
109
  if (maxSelections &&
36
110
  selectedDates.length >= maxSelections &&
37
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);
38
119
  return;
39
120
  }
40
- onDateSelect(clickedDate);
41
- };
42
- const handlePrevMonth = () => {
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(() => {
43
166
  setCurrentMonth(new Date(year, month - 1, 1));
44
- };
45
- const handleNextMonth = () => {
167
+ }, [year, month]);
168
+ const handleNextMonth = useCallback(() => {
46
169
  setCurrentMonth(new Date(year, month + 1, 1));
47
- };
48
- const isDateSelected = (day) => {
49
- const date = new Date(year, month, day);
50
- return selectedDates.some((selectedDate) => selectedDate.getTime() === date.getTime());
51
- };
52
- const isDateBooked = (day) => {
53
- const date = new Date(year, month, day);
54
- return bookedDates.some((bookedDate) => {
55
- const bookedDateOnly = new Date(bookedDate.getFullYear(), bookedDate.getMonth(), bookedDate.getDate());
56
- const currentDateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
57
- return bookedDateOnly.getTime() === currentDateOnly.getTime();
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 };
58
199
  });
59
- };
60
- const isDateDisabled = (day) => {
61
- const date = new Date(year, month, day);
62
- return date < today || isDateBooked(day);
63
- };
64
- // Generate calendar days
65
- const calendarDays = [];
66
- // Add empty cells for days before the first day of the month
67
- for (let i = 0; i < startingDayOfWeek; i++) {
68
- calendarDays.push(_jsx("div", { className: "h-10" }, `empty-${i}`));
69
- }
70
- // Add days of the month
71
- for (let day = 1; day <= daysInMonth; day++) {
72
- const isDisabled = isDateDisabled(day);
73
- const isSelected = isDateSelected(day);
74
- const isBooked = isDateBooked(day);
75
- const isPast = new Date(year, month, day) < today;
76
- let buttonClass = "h-8 w-8 rounded-full flex items-center justify-center text-[11px] transition-all duration-200 ";
77
- if (isDisabled) {
78
- if (isBooked) {
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) {
79
296
  buttonClass +=
80
- "bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200";
297
+ "bg-blue-500 text-white shadow-md transform scale-105 cursor-pointer";
81
298
  }
82
- else if (isPast) {
83
- buttonClass += "text-gray-300 cursor-not-allowed";
299
+ else {
300
+ buttonClass +=
301
+ "text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer";
84
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));
85
304
  }
86
- else if (isSelected) {
87
- buttonClass +=
88
- "bg-blue-500 text-white shadow-md transform scale-105 cursor-pointer";
89
- }
90
- else {
91
- buttonClass +=
92
- "text-gray-700 hover:bg-blue-50 hover:text-blue-600 cursor-pointer";
93
- }
94
- calendarDays.push(_jsx("button", { onClick: () => handleDateClick(day), disabled: isDisabled, className: buttonClass, title: isBooked ? "This date is already booked" : isPast ? "Past date" : "", children: day }, day));
95
- }
96
- return (_jsxs("div", { className: "calendar ", 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", children: _jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsxs("h3", { 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", children: _jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", 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" })] })] }) })] }));
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" })] })] })] }) }))] }));
97
439
  };
98
440
  export default Calendar;
@@ -13,6 +13,15 @@ export interface CalendarExtendedProps {
13
13
  start: MonthSelection;
14
14
  end: MonthSelection;
15
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
+ };
16
25
  minSelectableMonth?: MonthSelection;
17
26
  maxSelectableMonth?: MonthSelection;
18
27
  /** Convenience flag: when true, disallow selecting months beyond the current month */
@@ -24,4 +33,4 @@ export interface CalendarExtendedProps {
24
33
  bookedDates?: Date[];
25
34
  maxSelections?: number;
26
35
  }
27
- export default function CalendarExtended({ selectionGranularity, allowRange, startYear, endYear, selectedMonths, onMonthSelectionChange, onMonthRangeChange, minSelectableMonth, maxSelectableMonth, disableFutureMonths, containerHeight, enableYearTransition: _enableYearTransition, selectedDates, onDateSelect, bookedDates, maxSelections, }: Readonly<CalendarExtendedProps>): import("react/jsx-runtime").JSX.Element;
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;
@@ -16,7 +16,7 @@ const monthNames = [
16
16
  "November",
17
17
  "December",
18
18
  ];
19
- export default function CalendarExtended({ selectionGranularity = "day", allowRange = false, startYear, endYear, selectedMonths = [], onMonthSelectionChange, onMonthRangeChange, minSelectableMonth, maxSelectableMonth, disableFutureMonths, containerHeight = 560, enableYearTransition: _enableYearTransition = true, selectedDates = [], onDateSelect, bookedDates, maxSelections, }) {
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
20
  // Derive year bounds
21
21
  const currentYear = new Date().getFullYear();
22
22
  const DEFAULT_START_YEAR = 1900;
@@ -26,6 +26,9 @@ export default function CalendarExtended({ selectionGranularity = "day", allowRa
26
26
  // Internal range state if consumer enables range
27
27
  const [rangeStart, setRangeStart] = useState(null);
28
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;
29
32
  // Accessibility: grid id for aria-controls
30
33
  const gridIdRef = useRef(`calendar-extended-grid-${Math.random().toString(36).slice(2)}`);
31
34
  const allMonths = useMemo(() => {
@@ -42,10 +45,10 @@ export default function CalendarExtended({ selectionGranularity = "day", allowRa
42
45
  }, [fromYear, toYear]);
43
46
  const isSelected = (y, m) => selectedMonths.some((sel) => sel.year === y && sel.month === m);
44
47
  const isInRange = (y, m) => {
45
- if (!rangeStart || !rangeEnd)
48
+ if (!effectiveRangeStart || !effectiveRangeEnd)
46
49
  return false;
47
- const startKey = rangeStart.year * 12 + rangeStart.month;
48
- const endKey = rangeEnd.year * 12 + rangeEnd.month;
50
+ const startKey = effectiveRangeStart.year * 12 + effectiveRangeStart.month;
51
+ const endKey = effectiveRangeEnd.year * 12 + effectiveRangeEnd.month;
49
52
  const currentKey = y * 12 + m;
50
53
  const [minKey, maxKey] = startKey <= endKey ? [startKey, endKey] : [endKey, startKey];
51
54
  return currentKey >= minKey && currentKey <= maxKey;
@@ -109,8 +112,12 @@ export default function CalendarExtended({ selectionGranularity = "day", allowRa
109
112
  const selected = isSelected(y, m);
110
113
  const inRange = isInRange(y, m);
111
114
  const disabled = isMonthDisabled(y, m);
112
- const isStart = !!rangeStart && rangeStart.year === y && rangeStart.month === m;
113
- const isEnd = !!rangeEnd && rangeEnd.year === y && rangeEnd.month === 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;
114
121
  const isCurrent = (() => {
115
122
  const d = new Date();
116
123
  return d.getFullYear() === y && d.getMonth() === m;
@@ -120,13 +127,13 @@ export default function CalendarExtended({ selectionGranularity = "day", allowRa
120
127
  const stateClasses = disabled
121
128
  ? "opacity-50 cursor-not-allowed bg-gray-50 text-gray-400"
122
129
  : isStart && isEnd
123
- ? "bg-primaryblue/20 border-primaryblue text-white"
130
+ ? "bg-primaryblue text-white ring-2 ring-primaryblue"
124
131
  : isStart
125
- ? "bg-primaryblue/20 text-primaryblue ring-2 ring-primaryblue"
132
+ ? "bg-primaryblue text-white ring-2 ring-primaryblue"
126
133
  : isEnd
127
- ? "bg-primaryblue/20 text-primaryblue ring-2 ring-primaryblue/70"
134
+ ? "bg-primaryblue text-white ring-2 ring-primaryblue"
128
135
  : inRange
129
- ? "bg-primaryblue/10 text-primaryblue"
136
+ ? "bg-primaryblue/10 text-primaryblue ring-1 ring-primaryblue/30"
130
137
  : isSinglePreRange || selected
131
138
  ? "bg-primaryblue/5 text-primaryblue"
132
139
  : "bg-gray-100 hover:bg-gray-50";
@@ -31,9 +31,9 @@ const CustomDropdown = ({ label, placeholder = "Select an option", options, valu
31
31
  onChange("");
32
32
  setIsOpen(false);
33
33
  };
34
- return (_jsxs("div", { className: `w-full ${minimal ? "text-[11px]" : "text-base sm:text-xs"} ${className}`, ref: dropdownRef, children: [label && (_jsx("label", { className: `${labelClass} block mb-2 text-[10px] text-gray-500`, children: label })), _jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `${minimal
34
+ return (_jsxs("div", { className: `w-full ${minimal ? "text-[11px]" : "text-base sm:text-xs"}`, ref: dropdownRef, children: [label && (_jsx("label", { className: `${labelClass} block mb-2 text-[10px] text-gray-500`, children: label })), _jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `${minimal
35
35
  ? "flex gap-1 items-center"
36
- : "w-full p-0 px-4 sm:p-4 rounded-xl bg-gray-50 border outline-none text-left flex items-center justify-between h-13.5 sm:h-auto"} ${error ? "border-red-500" : "border-gray-200"} ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`, children: [_jsx("span", { className: `${selectedOption ? "text-gray-900" : "text-gray-400"}`, children: selectedOption ? selectedOption.label : placeholder }), !disabled && (_jsx(ChevronDownIcon, { className: `w-4 h-4 text-gray-800 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}` }))] }), isOpen && !disabled && (_jsx("div", { className: "absolute z-50 min-w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-lg max-h-60 overflow-y-auto", children: options.map((option) => (_jsxs("div", { className: `flex items-center justify-between hover:bg-gray-50 transition-colors ${value === option.value
36
+ : "w-full p-0 px-4 sm:p-4 rounded-xl bg-gray-50 border outline-none text-left flex items-center justify-between h-13.5 sm:h-auto"} ${error ? "border-red-500" : "border-gray-200"} ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"} ${className}`, children: [_jsx("span", { className: `${selectedOption ? "text-gray-900" : "text-gray-400"}`, children: selectedOption ? selectedOption.label : placeholder }), !disabled && (_jsx(ChevronDownIcon, { className: `w-4 h-4 text-gray-800 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}` }))] }), isOpen && !disabled && (_jsx("div", { className: "absolute z-50 min-w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-lg max-h-60 overflow-y-auto", children: options.map((option) => (_jsxs("div", { className: `flex items-center justify-between hover:bg-gray-50 transition-colors ${value === option.value
37
37
  ? "bg-blue-50 text-blue-600"
38
38
  : "text-gray-900"} first:rounded-t-xl last:rounded-b-xl`, children: [_jsx("button", { type: "button", onClick: () => handleOptionClick(option.value), className: "flex-1 text-left text-nowrap px-4 py-3", children: option.label }), value === option.value && (_jsx("button", { type: "button", onClick: handleClear, className: "p-0.5 hover:bg-gray-100 rounded-full transition-colors ml-2", children: _jsx(X, { className: "w-3 h-3 text-gray-400 hover:text-gray-600" }) }))] }, option.value))) }))] }), error && _jsx("p", { className: "mt-1 text-red-500", children: error })] }));
39
39
  };