@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.
Files changed (91) hide show
  1. package/README.md +117 -4
  2. package/dist/components/button/CircleBtn.d.ts +9 -0
  3. package/dist/components/button/CircleBtn.js +5 -0
  4. package/dist/components/button/ShadowButton.d.ts +8 -0
  5. package/dist/components/button/ShadowButton.js +5 -0
  6. package/dist/components/button/index.d.ts +12 -3
  7. package/dist/components/button/index.js +17 -3
  8. package/dist/components/core/Accordion.d.ts +11 -0
  9. package/dist/components/core/Accordion.js +7 -0
  10. package/dist/components/core/AddressInput.d.ts +34 -0
  11. package/dist/components/core/AddressInput.js +88 -0
  12. package/dist/components/core/BottomSheet.d.ts +10 -0
  13. package/dist/components/core/BottomSheet.js +15 -0
  14. package/dist/components/core/Calendar.d.ts +17 -0
  15. package/dist/components/core/Calendar.js +440 -0
  16. package/dist/components/core/CalendarExtended.d.ts +36 -0
  17. package/dist/components/core/CalendarExtended.js +154 -0
  18. package/dist/components/core/Checkbox.d.ts +13 -0
  19. package/dist/components/core/Checkbox.js +10 -0
  20. package/dist/components/core/CustomDropdown.d.ts +19 -0
  21. package/dist/components/core/CustomDropdown.js +41 -0
  22. package/dist/components/core/Modal.d.ts +12 -0
  23. package/dist/components/core/Modal.js +12 -0
  24. package/dist/components/core/NumberInput.d.ts +12 -0
  25. package/dist/components/core/NumberInput.js +34 -0
  26. package/dist/components/core/NumberInputWithCommas.d.ts +10 -0
  27. package/dist/components/core/NumberInputWithCommas.js +19 -0
  28. package/dist/components/core/OptionsDropdown.d.ts +35 -0
  29. package/dist/components/core/OptionsDropdown.js +50 -0
  30. package/dist/components/core/OtpInput.d.ts +9 -0
  31. package/dist/components/core/OtpInput.js +54 -0
  32. package/dist/components/core/PreviewArea.d.ts +18 -0
  33. package/dist/components/core/PreviewArea.js +31 -0
  34. package/dist/components/core/SelectionList.d.ts +16 -0
  35. package/dist/components/core/SelectionList.js +9 -0
  36. package/dist/components/core/SquareCheckbox.d.ts +10 -0
  37. package/dist/components/core/SquareCheckbox.js +5 -0
  38. package/dist/components/core/TabNavigation.d.ts +19 -0
  39. package/dist/components/core/TabNavigation.js +11 -0
  40. package/dist/components/core/Textarea.d.ts +10 -0
  41. package/dist/components/core/Textarea.js +8 -0
  42. package/dist/components/core/Toggle.d.ts +10 -0
  43. package/dist/components/core/Toggle.js +11 -0
  44. package/dist/components/core/Upload.d.ts +28 -0
  45. package/dist/components/core/Upload.js +76 -0
  46. package/dist/components/core/UploadArea.d.ts +14 -0
  47. package/dist/components/core/UploadArea.js +13 -0
  48. package/dist/components/core/input.d.ts +11 -0
  49. package/dist/components/core/input.js +24 -0
  50. package/dist/components/index.d.ts +41 -1
  51. package/dist/components/index.js +25 -1
  52. package/dist/components/navigation/Logo.d.ts +7 -0
  53. package/dist/components/navigation/Logo.js +7 -0
  54. package/dist/components/notification/GlobalNotificationProvider.d.ts +4 -0
  55. package/dist/components/notification/GlobalNotificationProvider.js +6 -0
  56. package/dist/components/notification/NotificationPopup.d.ts +1 -0
  57. package/dist/components/notification/NotificationPopup.js +73 -0
  58. package/dist/components/notification/notificationStore.d.ts +15 -0
  59. package/dist/components/notification/notificationStore.js +12 -0
  60. package/dist/components/svg/Crestbase.d.ts +6 -0
  61. package/dist/components/svg/Crestbase.js +5 -0
  62. package/dist/components/svg/CrestbaseColored.d.ts +6 -0
  63. package/dist/components/svg/CrestbaseColored.js +5 -0
  64. package/dist/components/svg/UploadIcon.d.ts +5 -0
  65. package/dist/components/svg/UploadIcon.js +4 -0
  66. package/dist/hooks/index.d.ts +4 -0
  67. package/dist/hooks/index.js +4 -0
  68. package/dist/hooks/useFileUpload.d.ts +35 -0
  69. package/dist/hooks/useFileUpload.js +219 -0
  70. package/dist/hooks/useFileUploadWithNotifications.d.ts +36 -0
  71. package/dist/hooks/useFileUploadWithNotifications.js +39 -0
  72. package/dist/hooks/useResponsive.d.ts +38 -0
  73. package/dist/hooks/useResponsive.js +97 -0
  74. package/dist/index.d.ts +3 -1
  75. package/dist/index.js +3 -1
  76. package/dist/styles.css +2 -0
  77. package/dist/types/property.d.ts +17 -0
  78. package/dist/types/property.js +2 -0
  79. package/dist/utils/fileValidation.d.ts +21 -0
  80. package/dist/utils/fileValidation.js +82 -0
  81. package/dist/utils/googleMapsLoader.d.ts +40 -0
  82. package/dist/utils/googleMapsLoader.js +108 -0
  83. package/dist/utils/index.d.ts +3 -0
  84. package/dist/utils/index.js +13 -0
  85. package/package.json +22 -12
  86. package/dist/components/button/index.d.ts.map +0 -1
  87. package/dist/components/button/index.js.map +0 -1
  88. package/dist/components/index.d.ts.map +0 -1
  89. package/dist/components/index.js.map +0 -1
  90. package/dist/index.d.ts.map +0 -1
  91. 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;