@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.
Files changed (58) hide show
  1. package/dist/__LIB_STORES__.js +30 -2
  2. package/dist/components/AboutShow/AboutShow.svelte +278 -0
  3. package/dist/components/AboutShow/AboutShow.svelte.d.ts +43 -0
  4. package/dist/components/AboutShow/AboutShow.svelte.d.ts.map +1 -0
  5. package/dist/components/Calendar/MiniMonthCalendar.svelte +1446 -0
  6. package/dist/components/Calendar/{Calendar.svelte.d.ts → MiniMonthCalendar.svelte.d.ts} +20 -21
  7. package/dist/components/Calendar/MiniMonthCalendar.svelte.d.ts.map +1 -0
  8. package/dist/components/DarkModeToggle.svelte +3 -1
  9. package/dist/components/DarkModeToggle.svelte.d.ts.map +1 -1
  10. package/dist/components/FAQs/FAQs.svelte +49 -0
  11. package/dist/components/{Calendar/QuarterView.svelte.d.ts → FAQs/FAQs.svelte.d.ts} +10 -10
  12. package/dist/components/FAQs/FAQs.svelte.d.ts.map +1 -0
  13. package/dist/components/Input/Input.svelte +100 -12
  14. package/dist/components/Input/Input.svelte.d.ts +12 -0
  15. package/dist/components/Input/Input.svelte.d.ts.map +1 -1
  16. package/dist/components/Input/OTPInput.svelte +1 -1
  17. package/dist/components/MonthSwitcher/MonthSwitcher.svelte +206 -0
  18. package/dist/components/MonthSwitcher/MonthSwitcher.svelte.d.ts +37 -0
  19. package/dist/components/MonthSwitcher/MonthSwitcher.svelte.d.ts.map +1 -0
  20. package/dist/components/OrderSummary/OrderSummary.svelte +553 -0
  21. package/dist/components/OrderSummary/OrderSummary.svelte.d.ts +65 -0
  22. package/dist/components/OrderSummary/OrderSummary.svelte.d.ts.map +1 -0
  23. package/dist/components/PublicCard/PublicCard.svelte +267 -0
  24. package/dist/components/{pages/performers/AvailabilityCalendarModal.svelte.d.ts → PublicCard/PublicCard.svelte.d.ts} +12 -14
  25. package/dist/components/PublicCard/PublicCard.svelte.d.ts.map +1 -0
  26. package/dist/components/ShowCard/ShowCard.svelte +240 -0
  27. package/dist/components/ShowCard/ShowCard.svelte.d.ts +39 -0
  28. package/dist/components/ShowCard/ShowCard.svelte.d.ts.map +1 -0
  29. package/dist/components/ShowTimeCard/ShowTimeCard.svelte +92 -0
  30. package/dist/components/{Calendar/QuarterView.stories.svelte.d.ts → ShowTimeCard/ShowTimeCard.svelte.d.ts} +17 -21
  31. package/dist/components/ShowTimeCard/ShowTimeCard.svelte.d.ts.map +1 -0
  32. package/dist/components/Spinner/Spinner.svelte +73 -17
  33. package/dist/components/Spinner/Spinner.svelte.d.ts +5 -3
  34. package/dist/components/Spinner/Spinner.svelte.d.ts.map +1 -1
  35. package/dist/components/pages/performers/ShowDetails.svelte.d.ts +2 -2
  36. package/dist/components/pages/performers/ShowItemCard.svelte.d.ts +6 -6
  37. package/dist/components/pages/performers/VenueItemCard.svelte.d.ts +2 -2
  38. package/dist/components/pages/shows/TabNavigation.svelte +7 -8
  39. package/dist/index.d.ts +8 -3
  40. package/dist/index.js +12 -3
  41. package/dist/services/EventService.js +75 -75
  42. package/dist/services/EventService.spec.js +217 -217
  43. package/dist/services/ShowService.spec.js +342 -342
  44. package/package.json +160 -160
  45. package/dist/components/Calendar/Calendar.spec.d.ts +0 -2
  46. package/dist/components/Calendar/Calendar.spec.d.ts.map +0 -1
  47. package/dist/components/Calendar/Calendar.spec.js +0 -131
  48. package/dist/components/Calendar/Calendar.svelte +0 -1115
  49. package/dist/components/Calendar/Calendar.svelte.d.ts.map +0 -1
  50. package/dist/components/Calendar/QuarterView.spec.d.ts +0 -2
  51. package/dist/components/Calendar/QuarterView.spec.d.ts.map +0 -1
  52. package/dist/components/Calendar/QuarterView.spec.js +0 -394
  53. package/dist/components/Calendar/QuarterView.stories.svelte +0 -134
  54. package/dist/components/Calendar/QuarterView.stories.svelte.d.ts.map +0 -1
  55. package/dist/components/Calendar/QuarterView.svelte +0 -736
  56. package/dist/components/Calendar/QuarterView.svelte.d.ts.map +0 -1
  57. package/dist/components/pages/performers/AvailabilityCalendarModal.svelte +0 -632
  58. 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>