@getmicdrop/svelte-components 2.0.13 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__LIB_STORES__.js +30 -2
- package/dist/components/AboutShow/AboutShow.svelte +278 -0
- package/dist/components/AboutShow/AboutShow.svelte.d.ts +43 -0
- package/dist/components/AboutShow/AboutShow.svelte.d.ts.map +1 -0
- package/dist/components/Calendar/MiniMonthCalendar.svelte +1446 -0
- package/dist/components/Calendar/{Calendar.svelte.d.ts → MiniMonthCalendar.svelte.d.ts} +20 -21
- package/dist/components/Calendar/MiniMonthCalendar.svelte.d.ts.map +1 -0
- package/dist/components/DarkModeToggle.svelte +3 -1
- package/dist/components/DarkModeToggle.svelte.d.ts.map +1 -1
- package/dist/components/FAQs/FAQs.svelte +49 -0
- package/dist/components/{Calendar/QuarterView.svelte.d.ts → FAQs/FAQs.svelte.d.ts} +10 -10
- package/dist/components/FAQs/FAQs.svelte.d.ts.map +1 -0
- package/dist/components/Input/Input.svelte +100 -12
- package/dist/components/Input/Input.svelte.d.ts +12 -0
- package/dist/components/Input/Input.svelte.d.ts.map +1 -1
- package/dist/components/Input/OTPInput.svelte +1 -1
- package/dist/components/MonthSwitcher/MonthSwitcher.svelte +206 -0
- package/dist/components/MonthSwitcher/MonthSwitcher.svelte.d.ts +37 -0
- package/dist/components/MonthSwitcher/MonthSwitcher.svelte.d.ts.map +1 -0
- package/dist/components/OrderSummary/OrderSummary.svelte +553 -0
- package/dist/components/OrderSummary/OrderSummary.svelte.d.ts +65 -0
- package/dist/components/OrderSummary/OrderSummary.svelte.d.ts.map +1 -0
- package/dist/components/PublicCard/PublicCard.svelte +267 -0
- package/dist/components/{pages/performers/AvailabilityCalendarModal.svelte.d.ts → PublicCard/PublicCard.svelte.d.ts} +12 -14
- package/dist/components/PublicCard/PublicCard.svelte.d.ts.map +1 -0
- package/dist/components/ShowCard/ShowCard.svelte +240 -0
- package/dist/components/ShowCard/ShowCard.svelte.d.ts +39 -0
- package/dist/components/ShowCard/ShowCard.svelte.d.ts.map +1 -0
- package/dist/components/ShowTimeCard/ShowTimeCard.svelte +92 -0
- package/dist/components/{Calendar/QuarterView.stories.svelte.d.ts → ShowTimeCard/ShowTimeCard.svelte.d.ts} +17 -21
- package/dist/components/ShowTimeCard/ShowTimeCard.svelte.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.svelte +73 -17
- package/dist/components/Spinner/Spinner.svelte.d.ts +5 -3
- package/dist/components/Spinner/Spinner.svelte.d.ts.map +1 -1
- package/dist/components/pages/performers/ShowDetails.svelte.d.ts +2 -2
- package/dist/components/pages/performers/ShowItemCard.svelte.d.ts +6 -6
- package/dist/components/pages/performers/VenueItemCard.svelte.d.ts +2 -2
- package/dist/components/pages/shows/TabNavigation.svelte +7 -8
- package/dist/index.d.ts +8 -3
- package/dist/index.js +12 -3
- package/dist/services/EventService.js +75 -75
- package/dist/services/EventService.spec.js +217 -217
- package/dist/services/ShowService.spec.js +342 -342
- package/package.json +160 -160
- package/dist/components/Calendar/Calendar.spec.d.ts +0 -2
- package/dist/components/Calendar/Calendar.spec.d.ts.map +0 -1
- package/dist/components/Calendar/Calendar.spec.js +0 -131
- package/dist/components/Calendar/Calendar.svelte +0 -1115
- package/dist/components/Calendar/Calendar.svelte.d.ts.map +0 -1
- package/dist/components/Calendar/QuarterView.spec.d.ts +0 -2
- package/dist/components/Calendar/QuarterView.spec.d.ts.map +0 -1
- package/dist/components/Calendar/QuarterView.spec.js +0 -394
- package/dist/components/Calendar/QuarterView.stories.svelte +0 -134
- package/dist/components/Calendar/QuarterView.stories.svelte.d.ts.map +0 -1
- package/dist/components/Calendar/QuarterView.svelte +0 -736
- package/dist/components/Calendar/QuarterView.svelte.d.ts.map +0 -1
- package/dist/components/pages/performers/AvailabilityCalendarModal.svelte +0 -632
- package/dist/components/pages/performers/AvailabilityCalendarModal.svelte.d.ts.map +0 -1
|
@@ -1,1115 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
3
|
-
import { browser } from '../../__LIB_ENVIRONMENT__.js';
|
|
4
|
-
import { Calendar } from '@fullcalendar/core';
|
|
5
|
-
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
6
|
-
import interactionPlugin from '@fullcalendar/interaction';
|
|
7
|
-
import Icon from '../Icons/Icon.svelte';
|
|
8
|
-
import Button from '../Button/Button.svelte';
|
|
9
|
-
|
|
10
|
-
// Props
|
|
11
|
-
export let variant = 'availability'; // 'availability' | 'scheduler' | 'display' | 'public'
|
|
12
|
-
export let view = 'dayGridMonth';
|
|
13
|
-
export let events = [];
|
|
14
|
-
export let selectedDates = []; // For availability - array of date strings like "2025-03-22"
|
|
15
|
-
export let showMonths = 1;
|
|
16
|
-
export let compact = false;
|
|
17
|
-
export let showNavigation = true;
|
|
18
|
-
export let showViewSwitcher = false;
|
|
19
|
-
export let readOnly = false;
|
|
20
|
-
export let saveStatus = ''; // 'saving' | 'saved' | ''
|
|
21
|
-
export let showLegend = true; // Show legend for availability variant
|
|
22
|
-
|
|
23
|
-
const dispatch = createEventDispatcher();
|
|
24
|
-
|
|
25
|
-
let calendarEl;
|
|
26
|
-
let calendar = null;
|
|
27
|
-
let currentDate = new Date();
|
|
28
|
-
|
|
29
|
-
// Haptic feedback helper
|
|
30
|
-
function triggerHaptic(style = 'light') {
|
|
31
|
-
if (typeof window === 'undefined') return;
|
|
32
|
-
|
|
33
|
-
if (window.webkit?.messageHandlers?.haptic) {
|
|
34
|
-
window.webkit.messageHandlers.haptic.postMessage(style);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (window.TapticEngine) {
|
|
39
|
-
if (style === 'light') {
|
|
40
|
-
window.TapticEngine.impact({ style: 'light' });
|
|
41
|
-
} else if (style === 'medium') {
|
|
42
|
-
window.TapticEngine.impact({ style: 'medium' });
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (navigator.vibrate) {
|
|
48
|
-
const duration = style === 'light' ? 10 : 20;
|
|
49
|
-
navigator.vibrate(duration);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check if a date is in the past
|
|
54
|
-
function isPastDate(date) {
|
|
55
|
-
// Get today's date in local time as YYYY-MM-DD
|
|
56
|
-
const today = new Date();
|
|
57
|
-
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
58
|
-
|
|
59
|
-
// Get the check date as YYYY-MM-DD string
|
|
60
|
-
const checkDateStr = formatDateString(date);
|
|
61
|
-
|
|
62
|
-
// Compare as strings (YYYY-MM-DD format sorts correctly)
|
|
63
|
-
return checkDateStr < todayStr;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Format date to YYYY-MM-DD string
|
|
67
|
-
function formatDateString(date) {
|
|
68
|
-
// If it's already a string in YYYY-MM-DD format, return it directly
|
|
69
|
-
if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
70
|
-
return date;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// For Date objects, use UTC methods to avoid timezone shifting
|
|
74
|
-
// FullCalendar passes dates that represent the calendar day in UTC
|
|
75
|
-
const d = new Date(date);
|
|
76
|
-
const year = d.getUTCFullYear();
|
|
77
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
78
|
-
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
79
|
-
return `${year}-${month}-${day}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check if a date is in a different month than currently displayed
|
|
83
|
-
function isOtherMonth(date) {
|
|
84
|
-
if (!calendar) return false;
|
|
85
|
-
const displayedDate = calendar.getDate();
|
|
86
|
-
const clickedDate = new Date(date);
|
|
87
|
-
return displayedDate.getMonth() !== clickedDate.getUTCMonth() ||
|
|
88
|
-
displayedDate.getFullYear() !== clickedDate.getUTCFullYear();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Handle date click for availability variant
|
|
92
|
-
function handleDateClick(info) {
|
|
93
|
-
if (variant !== 'availability' || readOnly) return;
|
|
94
|
-
|
|
95
|
-
// Ignore clicks on days from other months (prev/next month overflow)
|
|
96
|
-
if (isOtherMonth(info.date)) return;
|
|
97
|
-
|
|
98
|
-
const dateStr = formatDateString(info.date);
|
|
99
|
-
|
|
100
|
-
if (isPastDate(info.date)) return;
|
|
101
|
-
|
|
102
|
-
const isSelected = selectedDates.includes(dateStr);
|
|
103
|
-
|
|
104
|
-
if (isSelected) {
|
|
105
|
-
triggerHaptic('medium');
|
|
106
|
-
dispatch('dateSelect', { date: dateStr, selected: false });
|
|
107
|
-
} else {
|
|
108
|
-
triggerHaptic('light');
|
|
109
|
-
dispatch('dateSelect', { date: dateStr, selected: true });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Handle event click
|
|
114
|
-
function handleEventClick(info) {
|
|
115
|
-
dispatch('eventClick', { event: info.event });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Animated month change function
|
|
119
|
-
// direction: 1 = next, -1 = prev
|
|
120
|
-
function animateMonthChange(direction) {
|
|
121
|
-
if (!calendar || !calendarEl || isAnimating) return;
|
|
122
|
-
|
|
123
|
-
isAnimating = true;
|
|
124
|
-
const containerWidth = calendarEl.offsetWidth || 320;
|
|
125
|
-
|
|
126
|
-
// Clone the current calendar content
|
|
127
|
-
const clone = calendarEl.cloneNode(true);
|
|
128
|
-
clone.classList.add('calendar-clone');
|
|
129
|
-
clone.style.position = 'absolute';
|
|
130
|
-
clone.style.top = '0';
|
|
131
|
-
clone.style.left = '0';
|
|
132
|
-
clone.style.width = '100%';
|
|
133
|
-
clone.style.transform = 'translateX(0)';
|
|
134
|
-
clone.style.transition = 'none';
|
|
135
|
-
clone.style.pointerEvents = 'none';
|
|
136
|
-
clone.style.zIndex = '1';
|
|
137
|
-
|
|
138
|
-
// Insert clone into the slide container
|
|
139
|
-
const slideContainer = calendarEl.parentElement;
|
|
140
|
-
slideContainer.insertBefore(clone, calendarEl);
|
|
141
|
-
|
|
142
|
-
// Change month immediately
|
|
143
|
-
if (direction > 0) {
|
|
144
|
-
calendar.next();
|
|
145
|
-
} else {
|
|
146
|
-
calendar.prev();
|
|
147
|
-
}
|
|
148
|
-
currentDate = calendar.getDate();
|
|
149
|
-
|
|
150
|
-
// Update day cell classes immediately after month change, before animation
|
|
151
|
-
updateDayCells();
|
|
152
|
-
|
|
153
|
-
triggerHaptic('medium');
|
|
154
|
-
|
|
155
|
-
// Position new calendar off-screen on opposite side
|
|
156
|
-
const enterFrom = direction > 0 ? containerWidth : -containerWidth;
|
|
157
|
-
calendarEl.style.transition = 'none';
|
|
158
|
-
calendarEl.style.transform = `translateX(${enterFrom}px)`;
|
|
159
|
-
|
|
160
|
-
// Force reflow
|
|
161
|
-
calendarEl.offsetHeight;
|
|
162
|
-
|
|
163
|
-
// Animate clone out and new calendar in
|
|
164
|
-
const duration = 0.3;
|
|
165
|
-
const easing = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
166
|
-
|
|
167
|
-
clone.style.transition = `transform ${duration}s ${easing}`;
|
|
168
|
-
clone.style.transform = `translateX(${direction > 0 ? -containerWidth : containerWidth}px)`;
|
|
169
|
-
|
|
170
|
-
calendarEl.style.transition = `transform ${duration}s ${easing}`;
|
|
171
|
-
calendarEl.style.transform = 'translateX(0)';
|
|
172
|
-
|
|
173
|
-
// Animate header text
|
|
174
|
-
if (monthDisplayEl) {
|
|
175
|
-
monthDisplayEl.style.transition = 'none';
|
|
176
|
-
monthDisplayEl.style.transform = `translateX(${enterFrom * 0.3}px)`;
|
|
177
|
-
monthDisplayEl.style.opacity = '0';
|
|
178
|
-
monthDisplayEl.offsetHeight;
|
|
179
|
-
monthDisplayEl.style.transition = `transform ${duration}s ${easing}, opacity ${duration}s ${easing}`;
|
|
180
|
-
monthDisplayEl.style.transform = 'translateX(0)';
|
|
181
|
-
monthDisplayEl.style.opacity = '1';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Clean up after animation
|
|
185
|
-
setTimeout(() => {
|
|
186
|
-
if (clone.parentElement) {
|
|
187
|
-
clone.remove();
|
|
188
|
-
}
|
|
189
|
-
calendarEl.style.transition = '';
|
|
190
|
-
calendarEl.style.transform = '';
|
|
191
|
-
if (monthDisplayEl) {
|
|
192
|
-
monthDisplayEl.style.transition = '';
|
|
193
|
-
monthDisplayEl.style.transform = '';
|
|
194
|
-
monthDisplayEl.style.opacity = '';
|
|
195
|
-
}
|
|
196
|
-
isAnimating = false;
|
|
197
|
-
}, duration * 1000 + 50);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Navigation handlers
|
|
201
|
-
function handlePrev() {
|
|
202
|
-
animateMonthChange(-1);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function handleNext() {
|
|
206
|
-
animateMonthChange(1);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function handleToday() {
|
|
210
|
-
if (!calendar || !calendarEl || isAnimating) return;
|
|
211
|
-
|
|
212
|
-
const today = new Date();
|
|
213
|
-
const current = calendar.getDate();
|
|
214
|
-
|
|
215
|
-
// Determine direction: if today is before current, go left (-1), else go right (1)
|
|
216
|
-
// Compare year and month
|
|
217
|
-
const todayYM = today.getFullYear() * 12 + today.getMonth();
|
|
218
|
-
const currentYM = current.getFullYear() * 12 + current.getMonth();
|
|
219
|
-
|
|
220
|
-
if (todayYM === currentYM) {
|
|
221
|
-
// Already on today's month, just trigger haptic
|
|
222
|
-
triggerHaptic('light');
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const direction = todayYM > currentYM ? 1 : -1;
|
|
227
|
-
|
|
228
|
-
isAnimating = true;
|
|
229
|
-
const containerWidth = calendarEl.offsetWidth || 320;
|
|
230
|
-
|
|
231
|
-
// Clone the current calendar content
|
|
232
|
-
const clone = calendarEl.cloneNode(true);
|
|
233
|
-
clone.classList.add('calendar-clone');
|
|
234
|
-
clone.style.position = 'absolute';
|
|
235
|
-
clone.style.top = '0';
|
|
236
|
-
clone.style.left = '0';
|
|
237
|
-
clone.style.width = '100%';
|
|
238
|
-
clone.style.transform = 'translateX(0)';
|
|
239
|
-
clone.style.transition = 'none';
|
|
240
|
-
clone.style.pointerEvents = 'none';
|
|
241
|
-
clone.style.zIndex = '1';
|
|
242
|
-
|
|
243
|
-
const slideContainer = calendarEl.parentElement;
|
|
244
|
-
slideContainer.insertBefore(clone, calendarEl);
|
|
245
|
-
|
|
246
|
-
// Go to today
|
|
247
|
-
calendar.today();
|
|
248
|
-
currentDate = calendar.getDate();
|
|
249
|
-
|
|
250
|
-
// Update day cell classes immediately after month change, before animation
|
|
251
|
-
updateDayCells();
|
|
252
|
-
|
|
253
|
-
triggerHaptic('light');
|
|
254
|
-
|
|
255
|
-
// Position new calendar off-screen
|
|
256
|
-
const enterFrom = direction > 0 ? containerWidth : -containerWidth;
|
|
257
|
-
calendarEl.style.transition = 'none';
|
|
258
|
-
calendarEl.style.transform = `translateX(${enterFrom}px)`;
|
|
259
|
-
calendarEl.offsetHeight;
|
|
260
|
-
|
|
261
|
-
const duration = 0.3;
|
|
262
|
-
const easing = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
263
|
-
|
|
264
|
-
clone.style.transition = `transform ${duration}s ${easing}`;
|
|
265
|
-
clone.style.transform = `translateX(${direction > 0 ? -containerWidth : containerWidth}px)`;
|
|
266
|
-
|
|
267
|
-
calendarEl.style.transition = `transform ${duration}s ${easing}`;
|
|
268
|
-
calendarEl.style.transform = 'translateX(0)';
|
|
269
|
-
|
|
270
|
-
if (monthDisplayEl) {
|
|
271
|
-
monthDisplayEl.style.transition = 'none';
|
|
272
|
-
monthDisplayEl.style.transform = `translateX(${enterFrom * 0.3}px)`;
|
|
273
|
-
monthDisplayEl.style.opacity = '0';
|
|
274
|
-
monthDisplayEl.offsetHeight;
|
|
275
|
-
monthDisplayEl.style.transition = `transform ${duration}s ${easing}, opacity ${duration}s ${easing}`;
|
|
276
|
-
monthDisplayEl.style.transform = 'translateX(0)';
|
|
277
|
-
monthDisplayEl.style.opacity = '1';
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
setTimeout(() => {
|
|
281
|
-
if (clone.parentElement) clone.remove();
|
|
282
|
-
calendarEl.style.transition = '';
|
|
283
|
-
calendarEl.style.transform = '';
|
|
284
|
-
if (monthDisplayEl) {
|
|
285
|
-
monthDisplayEl.style.transition = '';
|
|
286
|
-
monthDisplayEl.style.transform = '';
|
|
287
|
-
monthDisplayEl.style.opacity = '';
|
|
288
|
-
}
|
|
289
|
-
isAnimating = false;
|
|
290
|
-
}, duration * 1000 + 50);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Smooth swipe with momentum
|
|
294
|
-
let touchStartX = 0;
|
|
295
|
-
let touchStartY = 0;
|
|
296
|
-
let touchStartTime = 0;
|
|
297
|
-
let lastTouchX = 0;
|
|
298
|
-
let lastTouchTime = 0;
|
|
299
|
-
let velocityX = 0;
|
|
300
|
-
let isSwiping = false;
|
|
301
|
-
let isAnimating = false;
|
|
302
|
-
let swipeOffset = 0;
|
|
303
|
-
|
|
304
|
-
// Threshold for triggering month change (percentage of calendar width)
|
|
305
|
-
const SWIPE_THRESHOLD = 0.15;
|
|
306
|
-
// Velocity threshold (pixels per ms)
|
|
307
|
-
const VELOCITY_THRESHOLD = 0.3;
|
|
308
|
-
|
|
309
|
-
function handleTouchStart(e) {
|
|
310
|
-
if (isAnimating) return;
|
|
311
|
-
|
|
312
|
-
touchStartX = e.touches[0].clientX;
|
|
313
|
-
touchStartY = e.touches[0].clientY;
|
|
314
|
-
touchStartTime = Date.now();
|
|
315
|
-
lastTouchX = touchStartX;
|
|
316
|
-
lastTouchTime = touchStartTime;
|
|
317
|
-
velocityX = 0;
|
|
318
|
-
isSwiping = false;
|
|
319
|
-
swipeOffset = 0;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function handleTouchMove(e) {
|
|
323
|
-
if (!touchStartX || isAnimating) return;
|
|
324
|
-
|
|
325
|
-
const touchCurrentX = e.touches[0].clientX;
|
|
326
|
-
const touchCurrentY = e.touches[0].clientY;
|
|
327
|
-
const diffX = touchStartX - touchCurrentX;
|
|
328
|
-
const diffY = touchStartY - touchCurrentY;
|
|
329
|
-
const now = Date.now();
|
|
330
|
-
|
|
331
|
-
// Determine if this is a horizontal swipe
|
|
332
|
-
if (!isSwiping && Math.abs(diffX) > 10) {
|
|
333
|
-
if (Math.abs(diffX) > Math.abs(diffY)) {
|
|
334
|
-
isSwiping = true;
|
|
335
|
-
} else {
|
|
336
|
-
// Vertical scroll - don't handle
|
|
337
|
-
touchStartX = 0;
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (isSwiping) {
|
|
343
|
-
e.preventDefault(); // Prevent page scroll during swipe
|
|
344
|
-
|
|
345
|
-
// Calculate velocity (pixels per ms)
|
|
346
|
-
const dt = now - lastTouchTime;
|
|
347
|
-
if (dt > 0) {
|
|
348
|
-
velocityX = (lastTouchX - touchCurrentX) / dt;
|
|
349
|
-
}
|
|
350
|
-
lastTouchX = touchCurrentX;
|
|
351
|
-
lastTouchTime = now;
|
|
352
|
-
|
|
353
|
-
// Update swipe offset with resistance at edges
|
|
354
|
-
swipeOffset = -diffX * 0.8; // 0.8 adds slight resistance
|
|
355
|
-
|
|
356
|
-
// Apply transform to calendar wrapper
|
|
357
|
-
if (calendarEl) {
|
|
358
|
-
calendarEl.style.transform = `translateX(${swipeOffset}px)`;
|
|
359
|
-
calendarEl.style.transition = 'none';
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Reference to month display for header animation
|
|
365
|
-
let monthDisplayEl;
|
|
366
|
-
|
|
367
|
-
function handleTouchEnd(e) {
|
|
368
|
-
if (!touchStartX || !isSwiping || isAnimating) {
|
|
369
|
-
touchStartX = 0;
|
|
370
|
-
touchStartY = 0;
|
|
371
|
-
isSwiping = false;
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const containerWidth = calendarEl?.offsetWidth || 320;
|
|
376
|
-
const swipePercent = Math.abs(swipeOffset) / containerWidth;
|
|
377
|
-
|
|
378
|
-
// Determine if we should change month based on:
|
|
379
|
-
// 1. Swipe distance threshold OR
|
|
380
|
-
// 2. Velocity threshold (for quick flicks)
|
|
381
|
-
const shouldChange = swipePercent > SWIPE_THRESHOLD || Math.abs(velocityX) > VELOCITY_THRESHOLD;
|
|
382
|
-
const direction = swipeOffset > 0 ? -1 : 1; // -1 = prev, 1 = next
|
|
383
|
-
|
|
384
|
-
isAnimating = true;
|
|
385
|
-
|
|
386
|
-
if (shouldChange) {
|
|
387
|
-
// Clone the current calendar content
|
|
388
|
-
const clone = calendarEl.cloneNode(true);
|
|
389
|
-
clone.classList.add('calendar-clone');
|
|
390
|
-
clone.style.position = 'absolute';
|
|
391
|
-
clone.style.top = '0';
|
|
392
|
-
clone.style.left = '0';
|
|
393
|
-
clone.style.width = '100%';
|
|
394
|
-
clone.style.transform = `translateX(${swipeOffset}px)`;
|
|
395
|
-
clone.style.transition = 'none';
|
|
396
|
-
clone.style.pointerEvents = 'none';
|
|
397
|
-
clone.style.zIndex = '1';
|
|
398
|
-
|
|
399
|
-
// Insert clone into the slide container (parent of calendar wrapper)
|
|
400
|
-
const slideContainer = calendarEl.parentElement;
|
|
401
|
-
slideContainer.insertBefore(clone, calendarEl);
|
|
402
|
-
|
|
403
|
-
// Change month immediately (but calendar is hidden/positioned off-screen)
|
|
404
|
-
if (direction > 0) {
|
|
405
|
-
if (calendar) {
|
|
406
|
-
calendar.next();
|
|
407
|
-
currentDate = calendar.getDate();
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
if (calendar) {
|
|
411
|
-
calendar.prev();
|
|
412
|
-
currentDate = calendar.getDate();
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Update day cell classes immediately after month change, before animation
|
|
417
|
-
updateDayCells();
|
|
418
|
-
|
|
419
|
-
triggerHaptic('medium');
|
|
420
|
-
|
|
421
|
-
// Position new calendar off-screen on opposite side
|
|
422
|
-
const enterFrom = direction > 0 ? containerWidth : -containerWidth;
|
|
423
|
-
calendarEl.style.transition = 'none';
|
|
424
|
-
calendarEl.style.transform = `translateX(${enterFrom}px)`;
|
|
425
|
-
|
|
426
|
-
// Force reflow
|
|
427
|
-
calendarEl.offsetHeight;
|
|
428
|
-
|
|
429
|
-
// Animate clone out and new calendar in
|
|
430
|
-
const duration = 0.3;
|
|
431
|
-
const easing = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
432
|
-
|
|
433
|
-
clone.style.transition = `transform ${duration}s ${easing}`;
|
|
434
|
-
clone.style.transform = `translateX(${direction > 0 ? -containerWidth : containerWidth}px)`;
|
|
435
|
-
|
|
436
|
-
calendarEl.style.transition = `transform ${duration}s ${easing}`;
|
|
437
|
-
calendarEl.style.transform = 'translateX(0)';
|
|
438
|
-
|
|
439
|
-
// Animate header text
|
|
440
|
-
if (monthDisplayEl) {
|
|
441
|
-
monthDisplayEl.style.transition = 'none';
|
|
442
|
-
monthDisplayEl.style.transform = `translateX(${enterFrom * 0.3}px)`;
|
|
443
|
-
monthDisplayEl.style.opacity = '0';
|
|
444
|
-
monthDisplayEl.offsetHeight; // Force reflow
|
|
445
|
-
monthDisplayEl.style.transition = `transform ${duration}s ${easing}, opacity ${duration}s ${easing}`;
|
|
446
|
-
monthDisplayEl.style.transform = 'translateX(0)';
|
|
447
|
-
monthDisplayEl.style.opacity = '1';
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Clean up after animation
|
|
451
|
-
setTimeout(() => {
|
|
452
|
-
if (clone.parentElement) {
|
|
453
|
-
clone.remove();
|
|
454
|
-
}
|
|
455
|
-
calendarEl.style.transition = '';
|
|
456
|
-
calendarEl.style.transform = '';
|
|
457
|
-
if (monthDisplayEl) {
|
|
458
|
-
monthDisplayEl.style.transition = '';
|
|
459
|
-
monthDisplayEl.style.transform = '';
|
|
460
|
-
monthDisplayEl.style.opacity = '';
|
|
461
|
-
}
|
|
462
|
-
isAnimating = false;
|
|
463
|
-
}, duration * 1000 + 50);
|
|
464
|
-
} else {
|
|
465
|
-
// Snap back to original position
|
|
466
|
-
if (calendarEl) {
|
|
467
|
-
calendarEl.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
468
|
-
calendarEl.style.transform = 'translateX(0)';
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
setTimeout(() => {
|
|
472
|
-
calendarEl.style.transition = '';
|
|
473
|
-
calendarEl.style.transform = '';
|
|
474
|
-
isAnimating = false;
|
|
475
|
-
}, 300);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Reset touch state
|
|
479
|
-
touchStartX = 0;
|
|
480
|
-
touchStartY = 0;
|
|
481
|
-
isSwiping = false;
|
|
482
|
-
swipeOffset = 0;
|
|
483
|
-
velocityX = 0;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Custom day cell rendering for availability variant
|
|
487
|
-
function dayCellDidMount(info) {
|
|
488
|
-
if (variant !== 'availability') return;
|
|
489
|
-
|
|
490
|
-
const dateStr = formatDateString(info.date);
|
|
491
|
-
const isSelected = selectedDates.includes(dateStr);
|
|
492
|
-
const isPast = isPastDate(info.date);
|
|
493
|
-
|
|
494
|
-
// Add classes to the cell itself (for full-box highlighting)
|
|
495
|
-
if (isSelected && !isPast) {
|
|
496
|
-
info.el.classList.add('day-selected');
|
|
497
|
-
}
|
|
498
|
-
if (isPast) {
|
|
499
|
-
info.el.classList.add('day-past');
|
|
500
|
-
}
|
|
501
|
-
if (isPast && isSelected) {
|
|
502
|
-
info.el.classList.add('day-past-selected');
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Update day cell classes when selectedDates change or month changes
|
|
507
|
-
function updateDayCells() {
|
|
508
|
-
if (!calendar || !calendarEl) return;
|
|
509
|
-
|
|
510
|
-
const dayCells = calendarEl.querySelectorAll('.fc-daygrid-day');
|
|
511
|
-
dayCells.forEach((cell) => {
|
|
512
|
-
const dateStr = cell.getAttribute('data-date');
|
|
513
|
-
if (!dateStr) return;
|
|
514
|
-
|
|
515
|
-
const isSelected = selectedDates.includes(dateStr);
|
|
516
|
-
// Pass the date string directly - formatDateString will handle it
|
|
517
|
-
const isPast = isPastDate(dateStr);
|
|
518
|
-
|
|
519
|
-
// Remove previous classes
|
|
520
|
-
cell.classList.remove('day-selected', 'day-past-selected', 'day-past');
|
|
521
|
-
|
|
522
|
-
// Add past class
|
|
523
|
-
if (isPast) {
|
|
524
|
-
cell.classList.add('day-past');
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Add selection classes
|
|
528
|
-
if (isSelected && !isPast) {
|
|
529
|
-
cell.classList.add('day-selected');
|
|
530
|
-
}
|
|
531
|
-
if (isPast && isSelected) {
|
|
532
|
-
cell.classList.add('day-past-selected');
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Watch for selectedDates changes
|
|
538
|
-
$: if (browser && calendar && selectedDates) {
|
|
539
|
-
updateDayCells();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Track current breakpoint (set on mount, no resize listener)
|
|
543
|
-
let currentBreakpoint = 'mobile';
|
|
544
|
-
|
|
545
|
-
function getBreakpoint(width) {
|
|
546
|
-
if (width >= 1024) return 'desktop';
|
|
547
|
-
if (width >= 768) return 'tablet';
|
|
548
|
-
return 'mobile';
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
onMount(() => {
|
|
552
|
-
if (!browser) return;
|
|
553
|
-
|
|
554
|
-
const plugins = [dayGridPlugin];
|
|
555
|
-
|
|
556
|
-
if (variant === 'availability' || variant === 'scheduler') {
|
|
557
|
-
plugins.push(interactionPlugin);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
calendar = new Calendar(calendarEl, {
|
|
561
|
-
plugins,
|
|
562
|
-
initialView: view,
|
|
563
|
-
initialDate: currentDate,
|
|
564
|
-
headerToolbar: false, // We build our own header
|
|
565
|
-
dayMaxEvents: true,
|
|
566
|
-
selectable: variant === 'availability' && !readOnly,
|
|
567
|
-
editable: variant === 'scheduler' && !readOnly,
|
|
568
|
-
dateClick: handleDateClick,
|
|
569
|
-
eventClick: handleEventClick,
|
|
570
|
-
dayCellDidMount: dayCellDidMount,
|
|
571
|
-
firstDay: 0, // Sunday
|
|
572
|
-
dayHeaderFormat: { weekday: 'short' },
|
|
573
|
-
height: 'auto',
|
|
574
|
-
showNonCurrentDates: true, // Always show other month days
|
|
575
|
-
fixedWeekCount: true, // Always show 6 weeks for consistent height
|
|
576
|
-
events: variant === 'availability' ? [] : events,
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
calendar.render();
|
|
580
|
-
|
|
581
|
-
// Set initial breakpoint - no resize listener needed
|
|
582
|
-
// CSS handles the sizing via media queries, and FullCalendar's updateSize()
|
|
583
|
-
// causes rendering glitches during resize. The calendar renders correctly
|
|
584
|
-
// on initial load at each breakpoint.
|
|
585
|
-
currentBreakpoint = getBreakpoint(window.innerWidth);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
onDestroy(() => {
|
|
589
|
-
if (calendar) {
|
|
590
|
-
calendar.destroy();
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// Format current month/year for header
|
|
595
|
-
$: headerText = new Intl.DateTimeFormat('en', {
|
|
596
|
-
month: 'long',
|
|
597
|
-
year: 'numeric'
|
|
598
|
-
}).format(currentDate);
|
|
599
|
-
</script>
|
|
600
|
-
|
|
601
|
-
<div
|
|
602
|
-
class="calendar-container"
|
|
603
|
-
class:compact
|
|
604
|
-
on:touchstart={handleTouchStart}
|
|
605
|
-
on:touchmove={handleTouchMove}
|
|
606
|
-
on:touchend={handleTouchEnd}
|
|
607
|
-
>
|
|
608
|
-
{#if showNavigation}
|
|
609
|
-
<header class="calendar-header">
|
|
610
|
-
<button
|
|
611
|
-
on:click={handlePrev}
|
|
612
|
-
class="nav-btn"
|
|
613
|
-
aria-label="Previous month"
|
|
614
|
-
>
|
|
615
|
-
<Icon name="ChevronLeft" size="16" />
|
|
616
|
-
</button>
|
|
617
|
-
|
|
618
|
-
<div class="month-display" bind:this={monthDisplayEl}>
|
|
619
|
-
<div class="month-content">
|
|
620
|
-
<div class="month-row">
|
|
621
|
-
<h2 class="month-title">{headerText.split(' ')[0]}</h2>
|
|
622
|
-
{#if saveStatus}
|
|
623
|
-
<span class="save-indicator">
|
|
624
|
-
{#if saveStatus === 'saving'}
|
|
625
|
-
<span class="animate-spin save-icon">
|
|
626
|
-
<Icon name="Reload" size="18" />
|
|
627
|
-
</span>
|
|
628
|
-
{:else if saveStatus === 'saved'}
|
|
629
|
-
<span class="save-icon save-icon--success">
|
|
630
|
-
<Icon name="CheckCircle" size="18" />
|
|
631
|
-
</span>
|
|
632
|
-
{/if}
|
|
633
|
-
</span>
|
|
634
|
-
{/if}
|
|
635
|
-
</div>
|
|
636
|
-
<span class="year-text">{headerText.split(' ')[1]}</span>
|
|
637
|
-
</div>
|
|
638
|
-
</div>
|
|
639
|
-
|
|
640
|
-
<button
|
|
641
|
-
on:click={handleNext}
|
|
642
|
-
class="nav-btn"
|
|
643
|
-
aria-label="Next month"
|
|
644
|
-
>
|
|
645
|
-
<Icon name="ChevronRight" size="16" />
|
|
646
|
-
</button>
|
|
647
|
-
</header>
|
|
648
|
-
{/if}
|
|
649
|
-
|
|
650
|
-
<div class="calendar-slide-container">
|
|
651
|
-
<div class="calendar-wrapper" bind:this={calendarEl}></div>
|
|
652
|
-
</div>
|
|
653
|
-
|
|
654
|
-
{#if variant === 'availability'}
|
|
655
|
-
<div class="action-buttons">
|
|
656
|
-
<Button
|
|
657
|
-
variant="blue-outline"
|
|
658
|
-
type="button"
|
|
659
|
-
size="lg"
|
|
660
|
-
on:click={handleToday}
|
|
661
|
-
>
|
|
662
|
-
Today
|
|
663
|
-
</Button>
|
|
664
|
-
</div>
|
|
665
|
-
|
|
666
|
-
{#if showLegend}
|
|
667
|
-
<div class="calendar-legend">
|
|
668
|
-
<div class="legend-item">
|
|
669
|
-
<span class="legend-dot legend-dot--available"></span>
|
|
670
|
-
<span class="legend-label">Available</span>
|
|
671
|
-
</div>
|
|
672
|
-
<div class="legend-item">
|
|
673
|
-
<span class="legend-dot legend-dot--unavailable"></span>
|
|
674
|
-
<span class="legend-label">Unavailable</span>
|
|
675
|
-
</div>
|
|
676
|
-
</div>
|
|
677
|
-
{/if}
|
|
678
|
-
{/if}
|
|
679
|
-
</div>
|
|
680
|
-
|
|
681
|
-
<style>
|
|
682
|
-
.calendar-container {
|
|
683
|
-
/* Mobile: full width - parent container handles gutters */
|
|
684
|
-
width: 100%;
|
|
685
|
-
margin: 0 auto;
|
|
686
|
-
text-align: center;
|
|
687
|
-
touch-action: pan-y pinch-zoom; /* Allow vertical scroll and zoom, handle horizontal ourselves */
|
|
688
|
-
overflow: hidden; /* Clip calendar during swipe animation */
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/* Tablet: larger fixed size */
|
|
692
|
-
@media (min-width: 768px) {
|
|
693
|
-
.calendar-container {
|
|
694
|
-
width: 600px;
|
|
695
|
-
max-width: 600px;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/* Desktop: even larger */
|
|
700
|
-
@media (min-width: 1024px) {
|
|
701
|
-
.calendar-container {
|
|
702
|
-
width: 700px;
|
|
703
|
-
max-width: 700px;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.calendar-container.compact {
|
|
708
|
-
width: 280px;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
.calendar-header {
|
|
712
|
-
display: grid;
|
|
713
|
-
grid-template-columns: 40px minmax(0, 1fr) 40px;
|
|
714
|
-
align-items: center;
|
|
715
|
-
gap: 8px;
|
|
716
|
-
margin-bottom: 1.5rem;
|
|
717
|
-
padding: 0 0.5rem;
|
|
718
|
-
position: sticky;
|
|
719
|
-
top: 0;
|
|
720
|
-
background: hsl(var(--BG-Primary));
|
|
721
|
-
z-index: 10;
|
|
722
|
-
-moz-user-select: none;
|
|
723
|
-
user-select: none;
|
|
724
|
-
-webkit-user-select: none;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
.nav-btn {
|
|
728
|
-
width: 40px;
|
|
729
|
-
height: 40px;
|
|
730
|
-
padding: 0.75rem;
|
|
731
|
-
display: flex;
|
|
732
|
-
align-items: center;
|
|
733
|
-
justify-content: center;
|
|
734
|
-
border: 1px solid hsl(var(--Stroke-Secondary));
|
|
735
|
-
border-radius: 0.5rem;
|
|
736
|
-
color: hsl(var(--Text-Tartiary));
|
|
737
|
-
background: hsl(var(--BG-Primary));
|
|
738
|
-
transition: opacity 0.15s ease;
|
|
739
|
-
-webkit-tap-highlight-color: transparent;
|
|
740
|
-
cursor: pointer;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
.nav-btn:active {
|
|
744
|
-
opacity: 0.4;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
.month-display {
|
|
748
|
-
position: relative;
|
|
749
|
-
display: flex;
|
|
750
|
-
align-items: center;
|
|
751
|
-
justify-content: center;
|
|
752
|
-
min-height: 48px;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
.month-content {
|
|
756
|
-
display: flex;
|
|
757
|
-
flex-direction: column;
|
|
758
|
-
align-items: center;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
.month-row {
|
|
762
|
-
display: flex;
|
|
763
|
-
align-items: center;
|
|
764
|
-
position: relative;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
.month-title {
|
|
768
|
-
font-size: 1.125rem;
|
|
769
|
-
font-weight: 500;
|
|
770
|
-
color: hsl(var(--Text-Primary));
|
|
771
|
-
line-height: 1.2;
|
|
772
|
-
margin: 0;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
.year-text {
|
|
776
|
-
font-size: 0.75rem;
|
|
777
|
-
color: hsl(var(--Text-Tartiary));
|
|
778
|
-
font-weight: 400;
|
|
779
|
-
line-height: 1;
|
|
780
|
-
margin-top: 2px;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
.save-indicator {
|
|
784
|
-
position: absolute;
|
|
785
|
-
left: 100%;
|
|
786
|
-
margin-left: 0.5rem;
|
|
787
|
-
display: flex;
|
|
788
|
-
align-items: center;
|
|
789
|
-
justify-content: center;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
.save-icon {
|
|
793
|
-
display: flex;
|
|
794
|
-
align-items: center;
|
|
795
|
-
justify-content: center;
|
|
796
|
-
color: hsl(var(--primary-700));
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
.save-icon--success {
|
|
800
|
-
color: hsl(142 76% 36%); /* green-600 */
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
.calendar-slide-container {
|
|
804
|
-
position: relative;
|
|
805
|
-
overflow: hidden;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
.calendar-wrapper {
|
|
809
|
-
border: 1px solid hsl(var(--Stroke-Secondary));
|
|
810
|
-
border-radius: 0.75rem;
|
|
811
|
-
overflow: hidden;
|
|
812
|
-
will-change: transform;
|
|
813
|
-
backface-visibility: hidden;
|
|
814
|
-
-webkit-backface-visibility: hidden;
|
|
815
|
-
position: relative;
|
|
816
|
-
z-index: 2;
|
|
817
|
-
background: hsl(var(--BG-Primary));
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
:global(.calendar-clone) {
|
|
821
|
-
border: 1px solid hsl(var(--Stroke-Secondary));
|
|
822
|
-
border-radius: 0.75rem;
|
|
823
|
-
overflow: hidden;
|
|
824
|
-
background: hsl(var(--BG-Primary));
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/*
|
|
828
|
-
* Fix FullCalendar column sync issues during resize
|
|
829
|
-
* Source: https://stackoverflow.com/questions/64178435/is-it-possible-to-adjust-the-width-of-the-fullcalendar
|
|
830
|
-
* The default FullCalendar sets various elements to fixed pixel widths (817px).
|
|
831
|
-
* Setting them to 100% keeps header and body columns in sync.
|
|
832
|
-
*/
|
|
833
|
-
:global(.calendar-wrapper .fc-col-header),
|
|
834
|
-
:global(.calendar-wrapper .fc-scrollgrid-sync-table),
|
|
835
|
-
:global(.calendar-wrapper .fc-daygrid-body) {
|
|
836
|
-
width: 100% !important;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/* Force equal column widths across all FullCalendar tables */
|
|
840
|
-
:global(.calendar-wrapper .fc table) {
|
|
841
|
-
table-layout: fixed !important;
|
|
842
|
-
width: 100% !important;
|
|
843
|
-
border-collapse: collapse;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
:global(.calendar-wrapper .fc-scrollgrid) {
|
|
847
|
-
border-collapse: collapse !important;
|
|
848
|
-
border-right: none !important;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
:global(.calendar-wrapper .fc-scrollgrid-section > td) {
|
|
852
|
-
padding: 0;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
:global(.calendar-wrapper .fc th),
|
|
856
|
-
:global(.calendar-wrapper .fc td) {
|
|
857
|
-
width: 14.2857% !important; /* 100% / 7 columns */
|
|
858
|
-
min-width: 0 !important;
|
|
859
|
-
max-width: none !important;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/* Remove right border from last column to prevent double border */
|
|
863
|
-
:global(.calendar-wrapper .fc th:last-child),
|
|
864
|
-
:global(.calendar-wrapper .fc td:last-child) {
|
|
865
|
-
border-right: none !important;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
:global(.calendar-wrapper .fc colgroup) {
|
|
869
|
-
display: none; /* Hide colgroup to prevent width conflicts */
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
.action-buttons {
|
|
873
|
-
display: flex;
|
|
874
|
-
justify-content: center;
|
|
875
|
-
margin-top: 1rem;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/* Legend */
|
|
879
|
-
.calendar-legend {
|
|
880
|
-
display: flex;
|
|
881
|
-
align-items: center;
|
|
882
|
-
justify-content: center;
|
|
883
|
-
gap: 1.5rem;
|
|
884
|
-
padding-top: 1rem;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.legend-item {
|
|
888
|
-
display: flex;
|
|
889
|
-
align-items: center;
|
|
890
|
-
gap: 0.5rem;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
.legend-dot {
|
|
894
|
-
width: 1.25rem;
|
|
895
|
-
height: 1.25rem;
|
|
896
|
-
border-radius: 4px;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
.legend-dot--available {
|
|
900
|
-
background-color: hsl(var(--primary-700));
|
|
901
|
-
border: 1px solid hsl(var(--primary-700));
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
.legend-dot--unavailable {
|
|
905
|
-
background-color: hsl(var(--BG-Secondary));
|
|
906
|
-
border: 2px solid hsl(var(--Text-Tartiary));
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
.legend-label {
|
|
910
|
-
font-size: 0.875rem;
|
|
911
|
-
color: hsl(var(--Text-Primary));
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/* FullCalendar CSS overrides */
|
|
915
|
-
:global(.fc) {
|
|
916
|
-
--fc-bg-color: hsl(var(--BG-Primary));
|
|
917
|
-
--fc-border-color: hsl(var(--Stroke-Secondary));
|
|
918
|
-
--fc-text-color: hsl(var(--Text-Primary));
|
|
919
|
-
--fc-today-bg-color: transparent;
|
|
920
|
-
--fc-highlight-color: hsl(var(--primary-50));
|
|
921
|
-
--fc-page-bg-color: hsl(var(--BG-Primary));
|
|
922
|
-
--fc-neutral-bg-color: hsl(var(--BG-Secondary));
|
|
923
|
-
--fc-neutral-text-color: hsl(var(--Text-Tartiary));
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
:global(.fc .fc-daygrid-day-frame) {
|
|
927
|
-
/* Mobile: compact cells */
|
|
928
|
-
min-height: 48px;
|
|
929
|
-
cursor: pointer;
|
|
930
|
-
-webkit-tap-highlight-color: transparent;
|
|
931
|
-
/* Center the day number vertically */
|
|
932
|
-
display: flex;
|
|
933
|
-
align-items: center;
|
|
934
|
-
justify-content: center;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
/* Tablet: taller cells */
|
|
938
|
-
@media (min-width: 768px) {
|
|
939
|
-
:global(.fc .fc-daygrid-day-frame) {
|
|
940
|
-
min-height: 56px;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/* Desktop: even taller cells */
|
|
945
|
-
@media (min-width: 1024px) {
|
|
946
|
-
:global(.fc .fc-daygrid-day-frame) {
|
|
947
|
-
min-height: 64px;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
:global(.fc .fc-daygrid-day:hover:not(.fc-day-other)) {
|
|
952
|
-
background: hsl(var(--BG-Secondary));
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
:global(.fc .fc-day-other),
|
|
956
|
-
:global(.fc .fc-day-other.day-selected),
|
|
957
|
-
:global(.fc .fc-day-other.day-past-selected),
|
|
958
|
-
:global(.fc .fc-day-other.day-past) {
|
|
959
|
-
background: hsl(var(--BG-Secondary) / 0.5) !important;
|
|
960
|
-
cursor: default;
|
|
961
|
-
pointer-events: none;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
:global(.fc .fc-day-other .fc-daygrid-day-number) {
|
|
965
|
-
visibility: hidden;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
:global(.fc .fc-col-header-cell) {
|
|
969
|
-
padding: 0.75rem 0;
|
|
970
|
-
font-size: 0.875rem;
|
|
971
|
-
font-weight: 500;
|
|
972
|
-
color: hsl(var(--Text-Tartiary));
|
|
973
|
-
background: hsl(var(--BG-Primary));
|
|
974
|
-
border-bottom: 1px solid hsl(var(--Stroke-Secondary));
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
:global(.fc .fc-col-header-cell-cushion) {
|
|
978
|
-
color: hsl(var(--Text-Tartiary));
|
|
979
|
-
text-decoration: none;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
:global(.fc .fc-scrollgrid) {
|
|
983
|
-
border: none !important;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
:global(.fc .fc-scrollgrid-section > td) {
|
|
987
|
-
border: none;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/* Custom day number styling */
|
|
991
|
-
:global(.fc .fc-daygrid-day-number) {
|
|
992
|
-
display: flex;
|
|
993
|
-
align-items: center;
|
|
994
|
-
justify-content: center;
|
|
995
|
-
font-size: 0.875rem;
|
|
996
|
-
font-weight: 500;
|
|
997
|
-
color: hsl(var(--Text-Primary));
|
|
998
|
-
padding: 0.5rem;
|
|
999
|
-
text-decoration: none;
|
|
1000
|
-
width: 100%;
|
|
1001
|
-
height: 100%;
|
|
1002
|
-
/* Fast tap animation - under 100ms feels instantaneous */
|
|
1003
|
-
transition: transform 0.08s ease-out;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/* Tap feedback animation - quick scale pulse */
|
|
1007
|
-
:global(.fc .fc-daygrid-day:not(.fc-day-other):not(.day-past):active .fc-daygrid-day-number) {
|
|
1008
|
-
transform: scale(0.85);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/* Add a brief background flash on the cell itself for more visible feedback */
|
|
1012
|
-
:global(.fc .fc-daygrid-day:not(.fc-day-other):not(.day-past)) {
|
|
1013
|
-
transition: background-color 0.08s ease-out;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
:global(.fc .fc-daygrid-day:not(.fc-day-other):not(.day-past):not(.day-selected):active) {
|
|
1017
|
-
background-color: hsl(var(--primary-100)) !important;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
:global(.fc .fc-daygrid-day.day-selected:active) {
|
|
1021
|
-
background-color: hsl(var(--primary-800)) !important;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/* Full-cell highlighting for selected days with glow animation */
|
|
1025
|
-
:global(.fc .fc-daygrid-day.day-selected),
|
|
1026
|
-
:global(.fc .fc-daygrid-day.fc-day-today.day-selected) {
|
|
1027
|
-
background: hsl(var(--primary-700)) !important;
|
|
1028
|
-
animation: selection-glow 0.3s ease-out;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
@keyframes selection-glow {
|
|
1032
|
-
0% {
|
|
1033
|
-
background: hsl(var(--primary-100)) !important;
|
|
1034
|
-
box-shadow: 0 0 0 3px hsla(var(--primary-500) / 0.3) inset;
|
|
1035
|
-
}
|
|
1036
|
-
50% {
|
|
1037
|
-
background: hsl(var(--primary-400)) !important;
|
|
1038
|
-
box-shadow: 0 0 0 2px hsla(var(--primary-500) / 0.2) inset;
|
|
1039
|
-
}
|
|
1040
|
-
100% {
|
|
1041
|
-
background: hsl(var(--primary-700)) !important;
|
|
1042
|
-
box-shadow: none;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
:global(.fc .fc-daygrid-day.day-selected .fc-daygrid-day-number),
|
|
1047
|
-
:global(.fc .fc-daygrid-day.fc-day-today.day-selected .fc-daygrid-day-number) {
|
|
1048
|
-
color: white;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
/* Ensure today doesn't have special background when not selected (but allow hover) */
|
|
1052
|
-
:global(.fc .fc-daygrid-day.fc-day-today:not(.day-selected):not(:hover)) {
|
|
1053
|
-
background: transparent !important;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
/* Past day styling */
|
|
1057
|
-
:global(.fc .fc-daygrid-day.day-past .fc-daygrid-day-number) {
|
|
1058
|
-
color: hsl(var(--Text-Tartiary) / 0.5);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
:global(.fc .fc-daygrid-day.day-past-selected) {
|
|
1062
|
-
background: hsl(var(--BG-Quaternary)) !important;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
:global(.fc .fc-daygrid-day.day-past-selected .fc-daygrid-day-number) {
|
|
1066
|
-
color: hsl(var(--Text-Tartiary));
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/* Active/press state for non-selected, non-past days */
|
|
1070
|
-
:global(.fc .fc-daygrid-day:not(.day-past):not(.day-selected):active) {
|
|
1071
|
-
background: hsl(var(--primary-100)) !important;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
:global(.fc .fc-daygrid-day.day-selected:active) {
|
|
1075
|
-
opacity: 0.8;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/* Other month days - must come AFTER selected/past rules to override them */
|
|
1079
|
-
:global(.fc .fc-daygrid-day.fc-day-other),
|
|
1080
|
-
:global(.fc .fc-daygrid-day.fc-day-other.day-selected),
|
|
1081
|
-
:global(.fc .fc-daygrid-day.fc-day-other.day-past),
|
|
1082
|
-
:global(.fc .fc-daygrid-day.fc-day-other.day-past-selected) {
|
|
1083
|
-
background: hsl(var(--BG-Secondary) / 0.5) !important;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
/* Hide default FullCalendar elements we don't need */
|
|
1087
|
-
:global(.fc .fc-daygrid-day-top) {
|
|
1088
|
-
justify-content: center;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
:global(.fc .fc-daygrid-day-events) {
|
|
1092
|
-
display: none;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
:global(.fc .fc-daygrid-day-bg) {
|
|
1096
|
-
display: none;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/* Animation */
|
|
1100
|
-
.animate-spin {
|
|
1101
|
-
animation: spin 1s linear infinite;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
@keyframes spin {
|
|
1105
|
-
from { transform: rotate(0deg); }
|
|
1106
|
-
to { transform: rotate(360deg); }
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1110
|
-
.nav-btn,
|
|
1111
|
-
:global(.fc .fc-daygrid-day-number) {
|
|
1112
|
-
transition: none;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
</style>
|