@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
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* MiniMonthCalendar - A pure Svelte mini-month calendar component
|
|
4
|
+
*
|
|
5
|
+
* Supports variants:
|
|
6
|
+
* - 'availability': For performer availability selection (click to toggle dates)
|
|
7
|
+
* - 'display': Read-only display of events/dates
|
|
8
|
+
* - 'public': Public-facing calendar with event cards
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Smooth swipe gestures with momentum
|
|
12
|
+
* - Direction-aware shadows during swipe
|
|
13
|
+
* - Previous/next month preview days
|
|
14
|
+
* - Full accessibility support
|
|
15
|
+
* - Reduced motion support
|
|
16
|
+
* - Apple Calendar-style minimal grid on mobile
|
|
17
|
+
*/
|
|
18
|
+
import { createEventDispatcher, onMount } from 'svelte';
|
|
19
|
+
import ChevronLeft from '../Icons/ChevronLeft.svelte';
|
|
20
|
+
import ChevronRight from '../Icons/ChevronRight.svelte';
|
|
21
|
+
import CheckCircle from '../Icons/CheckCircle.svelte';
|
|
22
|
+
import Spinner from '../Spinner/Spinner.svelte';
|
|
23
|
+
|
|
24
|
+
// Props
|
|
25
|
+
export let variant = 'availability'; // 'availability' | 'display' | 'public'
|
|
26
|
+
export let selectedDates = []; // Array of date strings like "2025-03-22"
|
|
27
|
+
export let events = []; // Array of event objects for display/public variants
|
|
28
|
+
export let saveStatus = ''; // 'saving' | 'saved' | ''
|
|
29
|
+
export let showLegend = true;
|
|
30
|
+
export let showNavigation = true;
|
|
31
|
+
export let showTodayButton = true;
|
|
32
|
+
export let readOnly = false;
|
|
33
|
+
export let showPartialPreview = true; // Show prev/next month days to fill grid
|
|
34
|
+
export let disablePastNavigation = false; // Prevent navigating to past months
|
|
35
|
+
|
|
36
|
+
const dispatch = createEventDispatcher();
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
let currentDate = new Date();
|
|
40
|
+
let currentYear = currentDate.getFullYear();
|
|
41
|
+
let currentMonth = currentDate.getMonth();
|
|
42
|
+
|
|
43
|
+
// Swipe gesture support
|
|
44
|
+
let touchStartX = 0;
|
|
45
|
+
let touchStartY = 0;
|
|
46
|
+
let touchStartTime = 0;
|
|
47
|
+
let lastTouchX = 0;
|
|
48
|
+
let lastTouchTime = 0;
|
|
49
|
+
let velocityX = 0;
|
|
50
|
+
let isSwiping = false;
|
|
51
|
+
let isAnimating = false;
|
|
52
|
+
let swipeOffset = 0;
|
|
53
|
+
let calendarGridElement;
|
|
54
|
+
let monthDisplayEl;
|
|
55
|
+
let prefersReducedMotion = false;
|
|
56
|
+
|
|
57
|
+
// Button pressed states for touch feedback
|
|
58
|
+
let prevPressed = false;
|
|
59
|
+
let nextPressed = false;
|
|
60
|
+
let todayPressed = false;
|
|
61
|
+
|
|
62
|
+
// Momentum animation
|
|
63
|
+
let momentumAnimationId = null;
|
|
64
|
+
const MOMENTUM_FRICTION = 0.92;
|
|
65
|
+
const MOMENTUM_MIN_VELOCITY = 0.05;
|
|
66
|
+
|
|
67
|
+
// Thresholds
|
|
68
|
+
const SWIPE_THRESHOLD = 0.15;
|
|
69
|
+
const VELOCITY_THRESHOLD = 0.3;
|
|
70
|
+
const DRAG_RESISTANCE = 0.8;
|
|
71
|
+
|
|
72
|
+
// Month names
|
|
73
|
+
const MONTH_NAMES = [
|
|
74
|
+
"January", "February", "March", "April", "May", "June",
|
|
75
|
+
"July", "August", "September", "October", "November", "December"
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
79
|
+
const DAY_LETTERS = ["S", "M", "T", "W", "T", "F", "S"];
|
|
80
|
+
|
|
81
|
+
// Today's date (local time)
|
|
82
|
+
const today = new Date();
|
|
83
|
+
const todayLocal = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
84
|
+
const todayLocalMonth = today.getMonth();
|
|
85
|
+
const todayLocalYear = today.getFullYear();
|
|
86
|
+
|
|
87
|
+
// Reactive: Check if at current month
|
|
88
|
+
$: isAtCurrentMonth = currentYear === todayLocalYear && currentMonth === todayLocalMonth;
|
|
89
|
+
$: canGoPrev = !disablePastNavigation || !isAtCurrentMonth;
|
|
90
|
+
|
|
91
|
+
// Check for reduced motion preference
|
|
92
|
+
onMount(() => {
|
|
93
|
+
if (typeof window !== 'undefined') {
|
|
94
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
95
|
+
prefersReducedMotion = mediaQuery.matches;
|
|
96
|
+
mediaQuery.addEventListener('change', (e) => {
|
|
97
|
+
prefersReducedMotion = e.matches;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Normalize date to YYYY-MM-DD string
|
|
103
|
+
function normalizeDate(date) {
|
|
104
|
+
const d = new Date(date);
|
|
105
|
+
const year = d.getFullYear();
|
|
106
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
107
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
108
|
+
return `${year}-${month}-${day}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get days in month
|
|
112
|
+
function getDaysInMonth(year, month) {
|
|
113
|
+
return new Date(year, month + 1, 0).getDate();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if date is today
|
|
117
|
+
function isToday(year, month, day) {
|
|
118
|
+
return (
|
|
119
|
+
year === todayLocal.getFullYear() &&
|
|
120
|
+
month === todayLocal.getMonth() &&
|
|
121
|
+
day === todayLocal.getDate()
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if date is in the past
|
|
126
|
+
function isPastDate(year, month, day) {
|
|
127
|
+
const checkDate = new Date(year, month, day);
|
|
128
|
+
return checkDate < todayLocal;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if date is selected
|
|
132
|
+
function isSelectedDate(dateStr) {
|
|
133
|
+
return selectedDates.includes(dateStr);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if date has events (for display/public variants)
|
|
137
|
+
function hasEventsOnDate(dateStr) {
|
|
138
|
+
return events.some(event => event.date === dateStr);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get events for a specific date
|
|
142
|
+
function getEventsForDate(dateStr) {
|
|
143
|
+
return events.filter(event => event.date === dateStr);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build calendar days array - reacts to selectedDates changes
|
|
147
|
+
$: daysInMonth = (() => {
|
|
148
|
+
// Reference selectedDates to ensure reactivity
|
|
149
|
+
const _ = selectedDates;
|
|
150
|
+
const days = getDaysInMonth(currentYear, currentMonth);
|
|
151
|
+
const firstDayOfWeek = new Date(currentYear, currentMonth, 1).getDay();
|
|
152
|
+
const result = [];
|
|
153
|
+
|
|
154
|
+
for (let i = 1; i <= days; i++) {
|
|
155
|
+
const dateStr = normalizeDate(new Date(currentYear, currentMonth, i));
|
|
156
|
+
result.push({
|
|
157
|
+
day: i,
|
|
158
|
+
dateStr,
|
|
159
|
+
startDay: firstDayOfWeek + i - 1,
|
|
160
|
+
isPast: isPastDate(currentYear, currentMonth, i),
|
|
161
|
+
isToday: isToday(currentYear, currentMonth, i),
|
|
162
|
+
isSelected: selectedDates.includes(dateStr),
|
|
163
|
+
hasEvents: hasEventsOnDate(dateStr),
|
|
164
|
+
events: getEventsForDate(dateStr)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
// Previous month preview days
|
|
171
|
+
$: prevMonthPreviewDays = (() => {
|
|
172
|
+
if (!showPartialPreview) return [];
|
|
173
|
+
const firstDayOfWeek = new Date(currentYear, currentMonth, 1).getDay();
|
|
174
|
+
if (firstDayOfWeek === 0) return [];
|
|
175
|
+
|
|
176
|
+
const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
|
177
|
+
const prevYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
|
178
|
+
const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth);
|
|
179
|
+
|
|
180
|
+
return Array.from({ length: firstDayOfWeek }, (_, i) => ({
|
|
181
|
+
day: daysInPrevMonth - firstDayOfWeek + i + 1,
|
|
182
|
+
month: prevMonth,
|
|
183
|
+
year: prevYear
|
|
184
|
+
}));
|
|
185
|
+
})();
|
|
186
|
+
|
|
187
|
+
// Next month preview days
|
|
188
|
+
$: nextMonthPreviewDays = (() => {
|
|
189
|
+
if (!showPartialPreview) return [];
|
|
190
|
+
const days = getDaysInMonth(currentYear, currentMonth);
|
|
191
|
+
const lastDayOfWeek = new Date(currentYear, currentMonth, days).getDay();
|
|
192
|
+
if (lastDayOfWeek === 6) return [];
|
|
193
|
+
|
|
194
|
+
const daysToShow = 6 - lastDayOfWeek;
|
|
195
|
+
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
|
196
|
+
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
|
197
|
+
|
|
198
|
+
return Array.from({ length: daysToShow }, (_, i) => ({
|
|
199
|
+
day: i + 1,
|
|
200
|
+
month: nextMonth,
|
|
201
|
+
year: nextYear
|
|
202
|
+
}));
|
|
203
|
+
})();
|
|
204
|
+
|
|
205
|
+
// Haptic feedback
|
|
206
|
+
function triggerHaptic(style = 'light') {
|
|
207
|
+
if (typeof window === 'undefined') return;
|
|
208
|
+
|
|
209
|
+
if (window.webkit?.messageHandlers?.haptic) {
|
|
210
|
+
window.webkit.messageHandlers.haptic.postMessage(style);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (window.TapticEngine) {
|
|
215
|
+
window.TapticEngine.impact({ style });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (navigator.vibrate) {
|
|
220
|
+
navigator.vibrate(style === 'light' ? 10 : 20);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle day tap (touchend) for mobile - bypasses click delay
|
|
225
|
+
function handleDayTap(e, day) {
|
|
226
|
+
// Only process if it was a tap, not a swipe
|
|
227
|
+
if (isSwiping) return;
|
|
228
|
+
if (readOnly) return;
|
|
229
|
+
if (variant === 'availability' && day.isPast) return;
|
|
230
|
+
|
|
231
|
+
// Process immediately on touchend
|
|
232
|
+
if (variant === 'availability') {
|
|
233
|
+
const isSelected = selectedDates.includes(day.dateStr);
|
|
234
|
+
|
|
235
|
+
if (isSelected) {
|
|
236
|
+
triggerHaptic('medium');
|
|
237
|
+
dispatch('dateSelect', { date: day.dateStr, selected: false });
|
|
238
|
+
} else {
|
|
239
|
+
triggerHaptic('light');
|
|
240
|
+
dispatch('dateSelect', { date: day.dateStr, selected: true });
|
|
241
|
+
}
|
|
242
|
+
} else if (variant === 'display' || variant === 'public') {
|
|
243
|
+
if (day.hasEvents) {
|
|
244
|
+
triggerHaptic('light');
|
|
245
|
+
dispatch('dayClick', { date: day.dateStr, events: day.events });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle day click (for desktop/keyboard)
|
|
251
|
+
function handleDayClick(day) {
|
|
252
|
+
// Don't trigger click if we were dragging/swiping
|
|
253
|
+
if (isDragging || isSwiping) return;
|
|
254
|
+
if (readOnly) return;
|
|
255
|
+
if (variant === 'availability' && day.isPast) return;
|
|
256
|
+
|
|
257
|
+
if (variant === 'availability') {
|
|
258
|
+
const isSelected = selectedDates.includes(day.dateStr);
|
|
259
|
+
|
|
260
|
+
if (isSelected) {
|
|
261
|
+
triggerHaptic('medium');
|
|
262
|
+
dispatch('dateSelect', { date: day.dateStr, selected: false });
|
|
263
|
+
} else {
|
|
264
|
+
triggerHaptic('light');
|
|
265
|
+
dispatch('dateSelect', { date: day.dateStr, selected: true });
|
|
266
|
+
}
|
|
267
|
+
} else if (variant === 'display' || variant === 'public') {
|
|
268
|
+
if (day.hasEvents) {
|
|
269
|
+
triggerHaptic('light');
|
|
270
|
+
dispatch('dayClick', { date: day.dateStr, events: day.events });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Navigation
|
|
276
|
+
function goToPrevMonth() {
|
|
277
|
+
if (isAnimating || !canGoPrev) return;
|
|
278
|
+
triggerHaptic('light');
|
|
279
|
+
animateMonthChange(-1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function goToNextMonth() {
|
|
283
|
+
if (isAnimating) return;
|
|
284
|
+
triggerHaptic('light');
|
|
285
|
+
animateMonthChange(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function goToToday() {
|
|
289
|
+
if (isAnimating || isAtCurrentMonth) return;
|
|
290
|
+
|
|
291
|
+
const todayYM = today.getFullYear() * 12 + today.getMonth();
|
|
292
|
+
const currentYM = currentYear * 12 + currentMonth;
|
|
293
|
+
|
|
294
|
+
const direction = todayYM > currentYM ? 1 : -1;
|
|
295
|
+
triggerHaptic('light');
|
|
296
|
+
animateMonthChange(direction, () => {
|
|
297
|
+
currentYear = today.getFullYear();
|
|
298
|
+
currentMonth = today.getMonth();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Month change with animation
|
|
303
|
+
function animateMonthChange(direction, customAction = null) {
|
|
304
|
+
if (isAnimating || !calendarGridElement) return;
|
|
305
|
+
|
|
306
|
+
if (prefersReducedMotion) {
|
|
307
|
+
if (customAction) {
|
|
308
|
+
customAction();
|
|
309
|
+
} else if (direction > 0) {
|
|
310
|
+
if (currentMonth === 11) {
|
|
311
|
+
currentMonth = 0;
|
|
312
|
+
currentYear++;
|
|
313
|
+
} else {
|
|
314
|
+
currentMonth++;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
if (currentMonth === 0) {
|
|
318
|
+
currentMonth = 11;
|
|
319
|
+
currentYear--;
|
|
320
|
+
} else {
|
|
321
|
+
currentMonth--;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
triggerHaptic('medium');
|
|
325
|
+
dispatch('monthChange', { year: currentYear, month: currentMonth });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
isAnimating = true;
|
|
330
|
+
const containerWidth = calendarGridElement.offsetWidth || 320;
|
|
331
|
+
|
|
332
|
+
// Clone current calendar
|
|
333
|
+
const clone = calendarGridElement.cloneNode(true);
|
|
334
|
+
clone.classList.add('calendar-clone');
|
|
335
|
+
clone.style.position = 'absolute';
|
|
336
|
+
clone.style.top = '0';
|
|
337
|
+
clone.style.left = '0';
|
|
338
|
+
clone.style.width = '100%';
|
|
339
|
+
clone.style.transform = swipeOffset ? `translateX(${swipeOffset}px)` : 'translateX(0)';
|
|
340
|
+
clone.style.transition = 'none';
|
|
341
|
+
clone.style.pointerEvents = 'none';
|
|
342
|
+
clone.style.zIndex = '1';
|
|
343
|
+
|
|
344
|
+
const slideContainer = calendarGridElement.parentElement;
|
|
345
|
+
slideContainer.style.position = 'relative';
|
|
346
|
+
slideContainer.style.overflow = 'hidden';
|
|
347
|
+
slideContainer.insertBefore(clone, calendarGridElement);
|
|
348
|
+
|
|
349
|
+
// Change month
|
|
350
|
+
if (customAction) {
|
|
351
|
+
customAction();
|
|
352
|
+
} else if (direction > 0) {
|
|
353
|
+
if (currentMonth === 11) {
|
|
354
|
+
currentMonth = 0;
|
|
355
|
+
currentYear++;
|
|
356
|
+
} else {
|
|
357
|
+
currentMonth++;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
if (currentMonth === 0) {
|
|
361
|
+
currentMonth = 11;
|
|
362
|
+
currentYear--;
|
|
363
|
+
} else {
|
|
364
|
+
currentMonth--;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
triggerHaptic('medium');
|
|
369
|
+
dispatch('monthChange', { year: currentYear, month: currentMonth });
|
|
370
|
+
|
|
371
|
+
// Position new calendar off-screen
|
|
372
|
+
const enterFrom = direction > 0 ? containerWidth : -containerWidth;
|
|
373
|
+
calendarGridElement.style.transition = 'none';
|
|
374
|
+
calendarGridElement.style.transform = `translateX(${enterFrom}px)`;
|
|
375
|
+
calendarGridElement.offsetHeight; // Force reflow
|
|
376
|
+
|
|
377
|
+
// Animate
|
|
378
|
+
const duration = 0.3;
|
|
379
|
+
const easing = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
380
|
+
|
|
381
|
+
clone.style.transition = `transform ${duration}s ${easing}`;
|
|
382
|
+
clone.style.transform = `translateX(${direction > 0 ? -containerWidth : containerWidth}px)`;
|
|
383
|
+
|
|
384
|
+
calendarGridElement.style.transition = `transform ${duration}s ${easing}`;
|
|
385
|
+
calendarGridElement.style.transform = 'translateX(0)';
|
|
386
|
+
|
|
387
|
+
// Animate header
|
|
388
|
+
if (monthDisplayEl) {
|
|
389
|
+
monthDisplayEl.style.transition = 'none';
|
|
390
|
+
monthDisplayEl.style.transform = `translateX(${enterFrom * 0.3}px)`;
|
|
391
|
+
monthDisplayEl.style.opacity = '0';
|
|
392
|
+
monthDisplayEl.offsetHeight;
|
|
393
|
+
monthDisplayEl.style.transition = `transform ${duration}s ${easing}, opacity ${duration}s ${easing}`;
|
|
394
|
+
monthDisplayEl.style.transform = 'translateX(0)';
|
|
395
|
+
monthDisplayEl.style.opacity = '1';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Cleanup
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
if (clone.parentElement) clone.remove();
|
|
401
|
+
if (calendarGridElement) {
|
|
402
|
+
calendarGridElement.style.transition = '';
|
|
403
|
+
calendarGridElement.style.transform = '';
|
|
404
|
+
}
|
|
405
|
+
if (monthDisplayEl) {
|
|
406
|
+
monthDisplayEl.style.transition = '';
|
|
407
|
+
monthDisplayEl.style.transform = '';
|
|
408
|
+
monthDisplayEl.style.opacity = '';
|
|
409
|
+
}
|
|
410
|
+
isAnimating = false;
|
|
411
|
+
swipeOffset = 0;
|
|
412
|
+
}, duration * 1000 + 50);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Mouse state for desktop dragging
|
|
416
|
+
let isDragging = false;
|
|
417
|
+
|
|
418
|
+
// Touch handlers
|
|
419
|
+
function handleTouchStart(e) {
|
|
420
|
+
if (isAnimating) return;
|
|
421
|
+
|
|
422
|
+
touchStartX = e.touches[0].clientX;
|
|
423
|
+
touchStartY = e.touches[0].clientY;
|
|
424
|
+
touchStartTime = Date.now();
|
|
425
|
+
lastTouchX = touchStartX;
|
|
426
|
+
lastTouchTime = touchStartTime;
|
|
427
|
+
velocityX = 0;
|
|
428
|
+
isSwiping = false;
|
|
429
|
+
isDragging = false; // Reset drag state on new touch
|
|
430
|
+
swipeOffset = 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function handleTouchMove(e) {
|
|
434
|
+
if (!touchStartX || isAnimating) return;
|
|
435
|
+
|
|
436
|
+
const touchCurrentX = e.touches[0].clientX;
|
|
437
|
+
const touchCurrentY = e.touches[0].clientY;
|
|
438
|
+
const diffX = touchStartX - touchCurrentX;
|
|
439
|
+
const diffY = touchStartY - touchCurrentY;
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
|
|
442
|
+
if (!isSwiping && Math.abs(diffX) > 10) {
|
|
443
|
+
if (Math.abs(diffX) > Math.abs(diffY)) {
|
|
444
|
+
isSwiping = true;
|
|
445
|
+
} else {
|
|
446
|
+
touchStartX = 0;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isSwiping) {
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
|
|
454
|
+
const dt = now - lastTouchTime;
|
|
455
|
+
if (dt > 0) {
|
|
456
|
+
velocityX = (lastTouchX - touchCurrentX) / dt;
|
|
457
|
+
}
|
|
458
|
+
lastTouchX = touchCurrentX;
|
|
459
|
+
lastTouchTime = now;
|
|
460
|
+
|
|
461
|
+
swipeOffset = -diffX * DRAG_RESISTANCE;
|
|
462
|
+
|
|
463
|
+
if (calendarGridElement) {
|
|
464
|
+
calendarGridElement.style.transform = `translateX(${swipeOffset}px)`;
|
|
465
|
+
calendarGridElement.style.transition = 'none';
|
|
466
|
+
|
|
467
|
+
// Direction-aware shadow
|
|
468
|
+
const shadowIntensity = Math.min(Math.abs(swipeOffset) / 80, 0.4);
|
|
469
|
+
const blurRadius = 12 + shadowIntensity * 24;
|
|
470
|
+
const spreadRadius = shadowIntensity * 4;
|
|
471
|
+
|
|
472
|
+
if (diffX > 0) {
|
|
473
|
+
calendarGridElement.style.boxShadow = `-${spreadRadius}px 0 ${blurRadius}px rgba(0,0,0,${shadowIntensity})`;
|
|
474
|
+
} else {
|
|
475
|
+
calendarGridElement.style.boxShadow = `${spreadRadius}px 0 ${blurRadius}px rgba(0,0,0,${shadowIntensity})`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function handleTouchEnd() {
|
|
482
|
+
if (!touchStartX || !isSwiping || isAnimating) {
|
|
483
|
+
touchStartX = 0;
|
|
484
|
+
touchStartY = 0;
|
|
485
|
+
isSwiping = false;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const containerWidth = calendarGridElement?.offsetWidth || 320;
|
|
490
|
+
const swipePercent = Math.abs(swipeOffset) / containerWidth;
|
|
491
|
+
const shouldChange = swipePercent > SWIPE_THRESHOLD || Math.abs(velocityX) > VELOCITY_THRESHOLD;
|
|
492
|
+
const direction = swipeOffset > 0 ? -1 : 1;
|
|
493
|
+
|
|
494
|
+
if (calendarGridElement) {
|
|
495
|
+
calendarGridElement.style.boxShadow = '';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (shouldChange) {
|
|
499
|
+
animateMonthChange(direction);
|
|
500
|
+
} else {
|
|
501
|
+
// Snap back
|
|
502
|
+
if (calendarGridElement) {
|
|
503
|
+
calendarGridElement.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
504
|
+
calendarGridElement.style.transform = 'translateX(0)';
|
|
505
|
+
|
|
506
|
+
setTimeout(() => {
|
|
507
|
+
if (calendarGridElement) {
|
|
508
|
+
calendarGridElement.style.transition = '';
|
|
509
|
+
calendarGridElement.style.transform = '';
|
|
510
|
+
}
|
|
511
|
+
}, 300);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
touchStartX = 0;
|
|
516
|
+
touchStartY = 0;
|
|
517
|
+
isSwiping = false;
|
|
518
|
+
swipeOffset = 0;
|
|
519
|
+
velocityX = 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Mouse handlers for desktop swiping
|
|
523
|
+
function handleMouseDown(e) {
|
|
524
|
+
if (isAnimating || e.button !== 0) return; // Only left click
|
|
525
|
+
|
|
526
|
+
isDragging = false; // Will be set to true on move
|
|
527
|
+
touchStartX = e.clientX;
|
|
528
|
+
touchStartY = e.clientY;
|
|
529
|
+
touchStartTime = Date.now();
|
|
530
|
+
lastTouchX = touchStartX;
|
|
531
|
+
lastTouchTime = touchStartTime;
|
|
532
|
+
velocityX = 0;
|
|
533
|
+
isSwiping = false;
|
|
534
|
+
swipeOffset = 0;
|
|
535
|
+
|
|
536
|
+
// Prevent text selection
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function handleMouseMove(e) {
|
|
541
|
+
if (!touchStartX || isAnimating) return;
|
|
542
|
+
|
|
543
|
+
const mouseCurrentX = e.clientX;
|
|
544
|
+
const mouseCurrentY = e.clientY;
|
|
545
|
+
const diffX = touchStartX - mouseCurrentX;
|
|
546
|
+
const diffY = touchStartY - mouseCurrentY;
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
|
|
549
|
+
// Detect drag (prevent accidental clicks)
|
|
550
|
+
if (!isDragging && Math.abs(diffX) > 5) {
|
|
551
|
+
isDragging = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!isSwiping && Math.abs(diffX) > 10) {
|
|
555
|
+
if (Math.abs(diffX) > Math.abs(diffY)) {
|
|
556
|
+
isSwiping = true;
|
|
557
|
+
} else {
|
|
558
|
+
touchStartX = 0;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (isSwiping) {
|
|
564
|
+
const dt = now - lastTouchTime;
|
|
565
|
+
if (dt > 0) {
|
|
566
|
+
velocityX = (lastTouchX - mouseCurrentX) / dt;
|
|
567
|
+
}
|
|
568
|
+
lastTouchX = mouseCurrentX;
|
|
569
|
+
lastTouchTime = now;
|
|
570
|
+
|
|
571
|
+
swipeOffset = -diffX * DRAG_RESISTANCE;
|
|
572
|
+
|
|
573
|
+
if (calendarGridElement) {
|
|
574
|
+
calendarGridElement.style.transform = `translateX(${swipeOffset}px)`;
|
|
575
|
+
calendarGridElement.style.transition = 'none';
|
|
576
|
+
|
|
577
|
+
// Direction-aware shadow
|
|
578
|
+
const shadowIntensity = Math.min(Math.abs(swipeOffset) / 80, 0.4);
|
|
579
|
+
const blurRadius = 12 + shadowIntensity * 24;
|
|
580
|
+
const spreadRadius = shadowIntensity * 4;
|
|
581
|
+
|
|
582
|
+
if (diffX > 0) {
|
|
583
|
+
calendarGridElement.style.boxShadow = `-${spreadRadius}px 0 ${blurRadius}px rgba(0,0,0,${shadowIntensity})`;
|
|
584
|
+
} else {
|
|
585
|
+
calendarGridElement.style.boxShadow = `${spreadRadius}px 0 ${blurRadius}px rgba(0,0,0,${shadowIntensity})`;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function handleMouseUp() {
|
|
592
|
+
if (!touchStartX || isAnimating) {
|
|
593
|
+
touchStartX = 0;
|
|
594
|
+
touchStartY = 0;
|
|
595
|
+
isDragging = false;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!isSwiping) {
|
|
600
|
+
touchStartX = 0;
|
|
601
|
+
touchStartY = 0;
|
|
602
|
+
isDragging = false;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const containerWidth = calendarGridElement?.offsetWidth || 320;
|
|
607
|
+
const swipePercent = Math.abs(swipeOffset) / containerWidth;
|
|
608
|
+
const shouldChange = swipePercent > SWIPE_THRESHOLD || Math.abs(velocityX) > VELOCITY_THRESHOLD;
|
|
609
|
+
const direction = swipeOffset > 0 ? -1 : 1;
|
|
610
|
+
|
|
611
|
+
if (calendarGridElement) {
|
|
612
|
+
calendarGridElement.style.boxShadow = '';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (shouldChange) {
|
|
616
|
+
animateMonthChange(direction);
|
|
617
|
+
} else {
|
|
618
|
+
// Snap back
|
|
619
|
+
if (calendarGridElement) {
|
|
620
|
+
calendarGridElement.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
621
|
+
calendarGridElement.style.transform = 'translateX(0)';
|
|
622
|
+
|
|
623
|
+
setTimeout(() => {
|
|
624
|
+
if (calendarGridElement) {
|
|
625
|
+
calendarGridElement.style.transition = '';
|
|
626
|
+
calendarGridElement.style.transform = '';
|
|
627
|
+
}
|
|
628
|
+
}, 300);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
touchStartX = 0;
|
|
633
|
+
touchStartY = 0;
|
|
634
|
+
isSwiping = false;
|
|
635
|
+
swipeOffset = 0;
|
|
636
|
+
velocityX = 0;
|
|
637
|
+
isDragging = false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function handleMouseLeave() {
|
|
641
|
+
if (isSwiping) {
|
|
642
|
+
handleMouseUp();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Keyboard navigation for accessibility
|
|
647
|
+
function handleDayKeydown(event, day) {
|
|
648
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
649
|
+
event.preventDefault();
|
|
650
|
+
handleDayClick(day);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Generate accessible label for a day
|
|
655
|
+
function getDayAriaLabel(day) {
|
|
656
|
+
const date = new Date(currentYear, currentMonth, day.day);
|
|
657
|
+
const dateStr = date.toLocaleDateString('en-US', {
|
|
658
|
+
weekday: 'long',
|
|
659
|
+
month: 'long',
|
|
660
|
+
day: 'numeric',
|
|
661
|
+
year: 'numeric'
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
let label = dateStr;
|
|
665
|
+
if (day.isToday) label += ', Today';
|
|
666
|
+
if (variant === 'availability') {
|
|
667
|
+
label += day.isSelected ? ', Available' : ', Not available';
|
|
668
|
+
if (!day.isPast) label += '. Press Enter to toggle.';
|
|
669
|
+
} else if (day.hasEvents) {
|
|
670
|
+
const count = day.events.length;
|
|
671
|
+
label += `, ${count} event${count > 1 ? 's' : ''}. Press Enter to view.`;
|
|
672
|
+
}
|
|
673
|
+
return label;
|
|
674
|
+
}
|
|
675
|
+
</script>
|
|
676
|
+
|
|
677
|
+
<div
|
|
678
|
+
class="mini-calendar-container"
|
|
679
|
+
class:compact={variant === 'display'}
|
|
680
|
+
on:touchstart={handleTouchStart}
|
|
681
|
+
on:touchmove={handleTouchMove}
|
|
682
|
+
on:touchend={handleTouchEnd}
|
|
683
|
+
on:mousedown={handleMouseDown}
|
|
684
|
+
on:mousemove={handleMouseMove}
|
|
685
|
+
on:mouseup={handleMouseUp}
|
|
686
|
+
on:mouseleave={handleMouseLeave}
|
|
687
|
+
>
|
|
688
|
+
<!-- Header: Month left, Nav right (like MonthSwitcher) -->
|
|
689
|
+
{#if showNavigation}
|
|
690
|
+
<header class="calendar-header">
|
|
691
|
+
<div class="month-display" bind:this={monthDisplayEl}>
|
|
692
|
+
<h2 class="month-title">{MONTH_NAMES[currentMonth]}</h2>
|
|
693
|
+
{#if saveStatus && variant === 'availability'}
|
|
694
|
+
<span class="save-indicator">
|
|
695
|
+
{#if saveStatus === 'saving'}
|
|
696
|
+
<Spinner size="sm" color="secondary" />
|
|
697
|
+
{:else if saveStatus === 'saved'}
|
|
698
|
+
<span class="save-icon save-icon--success">
|
|
699
|
+
<CheckCircle size="20" color="hsl(142 76% 36%)" />
|
|
700
|
+
</span>
|
|
701
|
+
{/if}
|
|
702
|
+
</span>
|
|
703
|
+
{/if}
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div class="nav-buttons">
|
|
707
|
+
<button
|
|
708
|
+
class="nav-btn"
|
|
709
|
+
class:pressed={prevPressed}
|
|
710
|
+
on:click={goToPrevMonth}
|
|
711
|
+
on:touchstart={() => prevPressed = true}
|
|
712
|
+
on:touchend={() => prevPressed = false}
|
|
713
|
+
on:touchcancel={() => prevPressed = false}
|
|
714
|
+
on:mousedown={() => prevPressed = true}
|
|
715
|
+
on:mouseup={() => prevPressed = false}
|
|
716
|
+
on:mouseleave={() => prevPressed = false}
|
|
717
|
+
disabled={!canGoPrev}
|
|
718
|
+
aria-label="Previous month"
|
|
719
|
+
>
|
|
720
|
+
<ChevronLeft size="20" />
|
|
721
|
+
</button>
|
|
722
|
+
|
|
723
|
+
{#if showTodayButton}
|
|
724
|
+
<button
|
|
725
|
+
class="today-btn"
|
|
726
|
+
class:pressed={todayPressed}
|
|
727
|
+
on:click={goToToday}
|
|
728
|
+
on:touchstart={() => todayPressed = true}
|
|
729
|
+
on:touchend={() => todayPressed = false}
|
|
730
|
+
on:touchcancel={() => todayPressed = false}
|
|
731
|
+
on:mousedown={() => todayPressed = true}
|
|
732
|
+
on:mouseup={() => todayPressed = false}
|
|
733
|
+
on:mouseleave={() => todayPressed = false}
|
|
734
|
+
disabled={isAtCurrentMonth}
|
|
735
|
+
aria-label="Go to current month"
|
|
736
|
+
>
|
|
737
|
+
Today
|
|
738
|
+
</button>
|
|
739
|
+
{/if}
|
|
740
|
+
|
|
741
|
+
<button
|
|
742
|
+
class="nav-btn"
|
|
743
|
+
class:pressed={nextPressed}
|
|
744
|
+
on:click={goToNextMonth}
|
|
745
|
+
on:touchstart={() => nextPressed = true}
|
|
746
|
+
on:touchend={() => nextPressed = false}
|
|
747
|
+
on:touchcancel={() => nextPressed = false}
|
|
748
|
+
on:mousedown={() => nextPressed = true}
|
|
749
|
+
on:mouseup={() => nextPressed = false}
|
|
750
|
+
on:mouseleave={() => nextPressed = false}
|
|
751
|
+
aria-label="Next month"
|
|
752
|
+
>
|
|
753
|
+
<ChevronRight size="20" />
|
|
754
|
+
</button>
|
|
755
|
+
</div>
|
|
756
|
+
</header>
|
|
757
|
+
{/if}
|
|
758
|
+
|
|
759
|
+
<!-- Calendar Grid -->
|
|
760
|
+
<div class="calendar-slide-container">
|
|
761
|
+
<div
|
|
762
|
+
bind:this={calendarGridElement}
|
|
763
|
+
class="calendar-grid"
|
|
764
|
+
role="grid"
|
|
765
|
+
aria-label="{MONTH_NAMES[currentMonth]} {currentYear}"
|
|
766
|
+
>
|
|
767
|
+
<!-- Day headers -->
|
|
768
|
+
{#each DAY_NAMES as dayName, i}
|
|
769
|
+
<div
|
|
770
|
+
class="day-header"
|
|
771
|
+
role="columnheader"
|
|
772
|
+
aria-label={["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][i]}
|
|
773
|
+
>
|
|
774
|
+
<span class="day-letter">{DAY_LETTERS[i]}</span>
|
|
775
|
+
<span class="day-abbrev">{dayName}</span>
|
|
776
|
+
</div>
|
|
777
|
+
{/each}
|
|
778
|
+
|
|
779
|
+
<!-- Previous month preview -->
|
|
780
|
+
{#each prevMonthPreviewDays as previewDay}
|
|
781
|
+
<div class="day-cell preview-day" role="gridcell" aria-disabled="true">
|
|
782
|
+
<span class="day-number">{previewDay.day}</span>
|
|
783
|
+
</div>
|
|
784
|
+
{/each}
|
|
785
|
+
|
|
786
|
+
<!-- Current month days -->
|
|
787
|
+
{#each daysInMonth as day (day.dateStr)}
|
|
788
|
+
<div
|
|
789
|
+
class="day-cell"
|
|
790
|
+
class:past-day={day.isPast}
|
|
791
|
+
class:today={day.isToday}
|
|
792
|
+
class:selected={variant === 'availability' && day.isSelected && !day.isPast}
|
|
793
|
+
class:past-selected={variant === 'availability' && day.isSelected && day.isPast}
|
|
794
|
+
class:has-events={variant !== 'availability' && day.hasEvents}
|
|
795
|
+
class:clickable={!readOnly && (variant === 'availability' ? !day.isPast : day.hasEvents)}
|
|
796
|
+
role="gridcell"
|
|
797
|
+
aria-label={getDayAriaLabel(day)}
|
|
798
|
+
aria-selected={day.isSelected}
|
|
799
|
+
aria-disabled={variant === 'availability' ? day.isPast : !day.hasEvents}
|
|
800
|
+
tabindex={(!readOnly && (variant === 'availability' ? !day.isPast : day.hasEvents)) ? 0 : -1}
|
|
801
|
+
on:click={() => handleDayClick(day)}
|
|
802
|
+
on:touchend|preventDefault={(e) => handleDayTap(e, day)}
|
|
803
|
+
on:keydown={(e) => handleDayKeydown(e, day)}
|
|
804
|
+
>
|
|
805
|
+
<span class="day-number">{day.day}</span>
|
|
806
|
+
{#if variant !== 'availability' && day.hasEvents}
|
|
807
|
+
<div class="event-dots">
|
|
808
|
+
{#each day.events.slice(0, 3) as _}
|
|
809
|
+
<span class="event-dot"></span>
|
|
810
|
+
{/each}
|
|
811
|
+
</div>
|
|
812
|
+
{/if}
|
|
813
|
+
</div>
|
|
814
|
+
{/each}
|
|
815
|
+
|
|
816
|
+
<!-- Next month preview -->
|
|
817
|
+
{#each nextMonthPreviewDays as previewDay}
|
|
818
|
+
<div class="day-cell preview-day" role="gridcell" aria-disabled="true">
|
|
819
|
+
<span class="day-number">{previewDay.day}</span>
|
|
820
|
+
</div>
|
|
821
|
+
{/each}
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<!-- Legend -->
|
|
826
|
+
{#if showLegend && variant === 'availability'}
|
|
827
|
+
<div class="calendar-legend">
|
|
828
|
+
<div class="legend-item">
|
|
829
|
+
<span class="legend-dot legend-dot--available"></span>
|
|
830
|
+
<span class="legend-label">Available</span>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="legend-item">
|
|
833
|
+
<span class="legend-dot legend-dot--unavailable"></span>
|
|
834
|
+
<span class="legend-label">Unavailable</span>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
{/if}
|
|
838
|
+
</div>
|
|
839
|
+
|
|
840
|
+
<style>
|
|
841
|
+
/* ==========================================================================
|
|
842
|
+
MINI MONTH CALENDAR - Evaluation Branch Styling
|
|
843
|
+
Apple Calendar-style minimal grid on mobile
|
|
844
|
+
========================================================================== */
|
|
845
|
+
|
|
846
|
+
.mini-calendar-container {
|
|
847
|
+
width: 100%;
|
|
848
|
+
max-width: 100%;
|
|
849
|
+
margin: 0 auto;
|
|
850
|
+
touch-action: pan-y pinch-zoom;
|
|
851
|
+
overflow: hidden;
|
|
852
|
+
-webkit-tap-highlight-color: transparent;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.mini-calendar-container.compact {
|
|
856
|
+
max-width: 320px;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* --------------------------------------------------------------------------
|
|
860
|
+
HEADER: Month left, Nav buttons right
|
|
861
|
+
-------------------------------------------------------------------------- */
|
|
862
|
+
.calendar-header {
|
|
863
|
+
display: flex;
|
|
864
|
+
align-items: center;
|
|
865
|
+
justify-content: space-between;
|
|
866
|
+
margin-bottom: 1rem;
|
|
867
|
+
-moz-user-select: none;
|
|
868
|
+
user-select: none;
|
|
869
|
+
-webkit-user-select: none;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.month-display {
|
|
873
|
+
display: flex;
|
|
874
|
+
align-items: center;
|
|
875
|
+
gap: 0.5rem;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.month-title {
|
|
879
|
+
font-size: 1.875rem;
|
|
880
|
+
font-weight: 600;
|
|
881
|
+
color: hsl(var(--Text-Primary));
|
|
882
|
+
line-height: 1.2;
|
|
883
|
+
margin: 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.save-indicator {
|
|
887
|
+
display: flex;
|
|
888
|
+
align-items: center;
|
|
889
|
+
justify-content: center;
|
|
890
|
+
margin-left: 0.25rem;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.save-icon {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
justify-content: center;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.save-icon--success {
|
|
900
|
+
/* CheckCircle color is passed as prop */
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.nav-buttons {
|
|
904
|
+
display: flex;
|
|
905
|
+
align-items: center;
|
|
906
|
+
gap: 0.5rem;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.nav-btn {
|
|
910
|
+
padding: 0.75rem;
|
|
911
|
+
margin: -0.375rem;
|
|
912
|
+
display: flex;
|
|
913
|
+
align-items: center;
|
|
914
|
+
justify-content: center;
|
|
915
|
+
border: none;
|
|
916
|
+
border-radius: 9999px;
|
|
917
|
+
background: transparent;
|
|
918
|
+
color: hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
919
|
+
cursor: pointer;
|
|
920
|
+
-webkit-tap-highlight-color: transparent;
|
|
921
|
+
touch-action: manipulation;
|
|
922
|
+
-webkit-user-select: none;
|
|
923
|
+
-moz-user-select: none;
|
|
924
|
+
user-select: none;
|
|
925
|
+
transition: transform 0.1s ease, color 0.15s ease, background-color 0.15s ease;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.nav-btn:disabled {
|
|
929
|
+
opacity: 0.5;
|
|
930
|
+
cursor: not-allowed;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
@media (hover: hover) and (pointer: fine) {
|
|
934
|
+
.nav-btn:hover:not(:disabled) {
|
|
935
|
+
background-color: hsl(var(--BG-Secondary));
|
|
936
|
+
color: hsl(var(--Text-Primary));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.nav-btn.pressed:not(:disabled) {
|
|
941
|
+
transform: scale(0.9);
|
|
942
|
+
background-color: hsl(var(--BG-Secondary));
|
|
943
|
+
color: hsl(var(--Text-Primary));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.nav-btn:focus {
|
|
947
|
+
outline: none;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.nav-btn:focus-visible {
|
|
951
|
+
outline: 2px solid hsl(var(--Brand-Primary));
|
|
952
|
+
outline-offset: 2px;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.today-btn {
|
|
956
|
+
font-size: 0.875rem;
|
|
957
|
+
font-weight: 500;
|
|
958
|
+
padding: 0.25rem 0.75rem;
|
|
959
|
+
border: 1px solid hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
960
|
+
border-radius: 0.5rem;
|
|
961
|
+
background: transparent;
|
|
962
|
+
color: hsl(var(--Text-Primary));
|
|
963
|
+
cursor: pointer;
|
|
964
|
+
-webkit-tap-highlight-color: transparent;
|
|
965
|
+
touch-action: manipulation;
|
|
966
|
+
-webkit-user-select: none;
|
|
967
|
+
-moz-user-select: none;
|
|
968
|
+
user-select: none;
|
|
969
|
+
transition: transform 0.1s ease, background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.today-btn:disabled {
|
|
973
|
+
opacity: 0.4;
|
|
974
|
+
cursor: not-allowed;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
@media (hover: hover) and (pointer: fine) {
|
|
978
|
+
.today-btn:hover:not(:disabled) {
|
|
979
|
+
background-color: hsl(var(--Brand-Primary));
|
|
980
|
+
color: white;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.today-btn.pressed:not(:disabled) {
|
|
985
|
+
transform: scale(0.95);
|
|
986
|
+
background-color: hsl(var(--Brand-Primary));
|
|
987
|
+
color: white;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
.today-btn:focus {
|
|
991
|
+
outline: none;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.today-btn:focus-visible {
|
|
995
|
+
outline: 2px solid hsl(var(--Brand-Primary));
|
|
996
|
+
outline-offset: 2px;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/* --------------------------------------------------------------------------
|
|
1000
|
+
CALENDAR GRID
|
|
1001
|
+
Desktop: full borders
|
|
1002
|
+
Mobile: horizontal lines only (Apple Calendar style)
|
|
1003
|
+
-------------------------------------------------------------------------- */
|
|
1004
|
+
.calendar-slide-container {
|
|
1005
|
+
position: relative;
|
|
1006
|
+
overflow: hidden;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.calendar-grid {
|
|
1010
|
+
display: grid;
|
|
1011
|
+
grid-template-columns: repeat(7, 1fr);
|
|
1012
|
+
gap: 0;
|
|
1013
|
+
background: hsl(var(--BG-Primary));
|
|
1014
|
+
will-change: transform;
|
|
1015
|
+
-moz-user-select: none;
|
|
1016
|
+
user-select: none;
|
|
1017
|
+
-webkit-user-select: none;
|
|
1018
|
+
/* Fixed height to prevent jumping between months with different row counts */
|
|
1019
|
+
/* 7 rows total: 1 header row + 6 content rows (max possible) */
|
|
1020
|
+
/* Mobile: header ~20px + 6 rows × 56px = 356px min */
|
|
1021
|
+
min-height: 356px;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
@media (min-width: 640px) {
|
|
1025
|
+
.calendar-grid {
|
|
1026
|
+
/* Desktop: header ~28px + 6 rows × 60px = 388px min */
|
|
1027
|
+
min-height: 388px;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
@media (min-width: 768px) {
|
|
1032
|
+
.calendar-grid {
|
|
1033
|
+
/* Desktop md: header ~28px + 6 rows × 70px = 448px min */
|
|
1034
|
+
min-height: 448px;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
@media (min-width: 1024px) {
|
|
1039
|
+
.calendar-grid {
|
|
1040
|
+
/* Desktop lg: header ~28px + 6 rows × 80px = 508px min */
|
|
1041
|
+
min-height: 508px;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
:global(.calendar-clone) {
|
|
1046
|
+
background: hsl(var(--BG-Primary));
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/* --------------------------------------------------------------------------
|
|
1050
|
+
DAY HEADERS
|
|
1051
|
+
Mobile: single letter, small text
|
|
1052
|
+
Desktop: 3-letter abbreviation
|
|
1053
|
+
-------------------------------------------------------------------------- */
|
|
1054
|
+
.day-header {
|
|
1055
|
+
display: flex;
|
|
1056
|
+
align-items: flex-end;
|
|
1057
|
+
justify-content: center;
|
|
1058
|
+
padding-bottom: 0.25rem;
|
|
1059
|
+
font-size: 0.6875rem;
|
|
1060
|
+
font-weight: 400;
|
|
1061
|
+
color: hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
1062
|
+
overflow: hidden;
|
|
1063
|
+
line-height: 1;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* Mobile: show single letter */
|
|
1067
|
+
.day-letter {
|
|
1068
|
+
display: block;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.day-abbrev {
|
|
1072
|
+
display: none;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/* Desktop: show 3-letter abbreviation */
|
|
1076
|
+
@media (min-width: 640px) {
|
|
1077
|
+
.day-header {
|
|
1078
|
+
padding-bottom: 0.5rem;
|
|
1079
|
+
padding-top: 0.25rem;
|
|
1080
|
+
justify-content: flex-end;
|
|
1081
|
+
padding-right: 0.5rem;
|
|
1082
|
+
font-size: 1.125rem;
|
|
1083
|
+
color: hsl(var(--Text-Primary));
|
|
1084
|
+
border-bottom: 1px solid hsl(var(--Stroke-Secondary));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
.day-letter {
|
|
1088
|
+
display: none;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.day-abbrev {
|
|
1092
|
+
display: block;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* --------------------------------------------------------------------------
|
|
1097
|
+
DAY CELLS
|
|
1098
|
+
Mobile: minimal lines (box-shadow for unbroken horizontal lines)
|
|
1099
|
+
Desktop: full grid borders
|
|
1100
|
+
-------------------------------------------------------------------------- */
|
|
1101
|
+
.day-cell {
|
|
1102
|
+
display: flex;
|
|
1103
|
+
flex-direction: column;
|
|
1104
|
+
align-items: center;
|
|
1105
|
+
justify-content: flex-start;
|
|
1106
|
+
padding: 2px 4px;
|
|
1107
|
+
min-height: 56px;
|
|
1108
|
+
-webkit-tap-highlight-color: transparent;
|
|
1109
|
+
/* Mobile: horizontal lines only using box-shadow */
|
|
1110
|
+
box-shadow: inset 0 -1px 0 0 hsl(var(--Stroke-Secondary));
|
|
1111
|
+
/* Disable iOS touch callout and selection */
|
|
1112
|
+
-webkit-touch-callout: none;
|
|
1113
|
+
-webkit-user-select: none;
|
|
1114
|
+
-moz-user-select: none;
|
|
1115
|
+
user-select: none;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/* Remove bottom shadow on last row (mobile) */
|
|
1119
|
+
.day-cell:nth-last-child(-n + 7) {
|
|
1120
|
+
box-shadow: none;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.day-cell.clickable {
|
|
1124
|
+
cursor: pointer;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/* Desktop: full grid borders */
|
|
1128
|
+
@media (min-width: 640px) {
|
|
1129
|
+
.day-cell {
|
|
1130
|
+
padding: 0.5rem;
|
|
1131
|
+
min-height: 60px;
|
|
1132
|
+
box-shadow: none;
|
|
1133
|
+
border-top: 1px solid hsl(var(--Stroke-Secondary));
|
|
1134
|
+
border-left: 1px solid hsl(var(--Stroke-Secondary));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/* Right border on last column (Saturday) */
|
|
1138
|
+
.day-cell:nth-child(7n + 14) {
|
|
1139
|
+
border-right: 1px solid hsl(var(--Stroke-Secondary));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/* Bottom border on last row */
|
|
1143
|
+
.day-cell:nth-last-child(-n + 7) {
|
|
1144
|
+
border-bottom: 1px solid hsl(var(--Stroke-Secondary));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
@media (min-width: 768px) {
|
|
1149
|
+
.day-cell {
|
|
1150
|
+
min-height: 70px;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
@media (min-width: 1024px) {
|
|
1155
|
+
.day-cell {
|
|
1156
|
+
min-height: 80px;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/* --------------------------------------------------------------------------
|
|
1161
|
+
DAY NUMBERS
|
|
1162
|
+
-------------------------------------------------------------------------- */
|
|
1163
|
+
.day-number {
|
|
1164
|
+
font-size: 0.875rem;
|
|
1165
|
+
font-weight: 500;
|
|
1166
|
+
color: hsl(var(--Text-Primary));
|
|
1167
|
+
width: 100%;
|
|
1168
|
+
text-align: center;
|
|
1169
|
+
/* Only transform transition for tactile tap feel - no color transition */
|
|
1170
|
+
transition: transform 0.1s ease-out;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
@media (min-width: 640px) {
|
|
1174
|
+
.day-number {
|
|
1175
|
+
font-size: 1.125rem;
|
|
1176
|
+
font-weight: 400;
|
|
1177
|
+
text-align: right;
|
|
1178
|
+
margin-bottom: 0.5rem;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* --------------------------------------------------------------------------
|
|
1183
|
+
DAY STATES
|
|
1184
|
+
-------------------------------------------------------------------------- */
|
|
1185
|
+
|
|
1186
|
+
/*
|
|
1187
|
+
* Interaction states - designed for clean, responsive feedback
|
|
1188
|
+
*
|
|
1189
|
+
* Desktop:
|
|
1190
|
+
* - Hover: subtle number shrink (no background change to avoid conflicts)
|
|
1191
|
+
* - Active (mousedown): stronger number shrink
|
|
1192
|
+
* - Selected stays blue regardless of hover
|
|
1193
|
+
*
|
|
1194
|
+
* Mobile:
|
|
1195
|
+
* - Tap: number shrinks on press
|
|
1196
|
+
* - No hover effects (touch devices don't have hover)
|
|
1197
|
+
*/
|
|
1198
|
+
|
|
1199
|
+
/* Hover state (desktop only) - subtle scale feedback, no background */
|
|
1200
|
+
@media (hover: hover) and (pointer: fine) {
|
|
1201
|
+
.day-cell.clickable:hover .day-number {
|
|
1202
|
+
transform: scale(0.96);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/* Active/click state - stronger shrink on press (desktop) */
|
|
1206
|
+
.day-cell.clickable:active .day-number {
|
|
1207
|
+
transform: scale(0.88);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/* Mobile: no transform animations - just instant state changes */
|
|
1212
|
+
|
|
1213
|
+
/* Past days (not available) - subtle gray to indicate past */
|
|
1214
|
+
.day-cell.past-day {
|
|
1215
|
+
background-color: hsl(var(--BG-Tertiary));
|
|
1216
|
+
cursor: default;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.day-cell.past-day .day-number {
|
|
1220
|
+
color: hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/* Past days that WERE available - faded blue tint to show history */
|
|
1224
|
+
.day-cell.past-selected {
|
|
1225
|
+
background-color: hsl(var(--primary-700) / 0.2);
|
|
1226
|
+
cursor: default;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.day-cell.past-selected .day-number {
|
|
1230
|
+
color: hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/* Preview days (prev/next month) - transparent, very faint text */
|
|
1234
|
+
.day-cell.preview-day {
|
|
1235
|
+
pointer-events: none;
|
|
1236
|
+
background-color: transparent;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.day-cell.preview-day .day-number {
|
|
1240
|
+
color: hsl(var(--Text-Tertiary, var(--Text-Tartiary)) / 0.4);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/* Today - no special color for availability variant, just normal text */
|
|
1244
|
+
/* Only highlight today in display/public variants */
|
|
1245
|
+
|
|
1246
|
+
/*
|
|
1247
|
+
* Selection/Deselection styles
|
|
1248
|
+
*/
|
|
1249
|
+
|
|
1250
|
+
/* Selected days */
|
|
1251
|
+
.day-cell.selected {
|
|
1252
|
+
background-color: hsl(var(--primary-700));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.day-cell.selected .day-number {
|
|
1256
|
+
color: white;
|
|
1257
|
+
font-weight: 600;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* On mobile, maintain the horizontal line even on selected days */
|
|
1261
|
+
@media (max-width: 639px) {
|
|
1262
|
+
.day-cell.selected {
|
|
1263
|
+
box-shadow: inset 0 -1px 0 0 hsl(var(--Stroke-Secondary)) !important;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.day-cell.selected:nth-last-child(-n + 7) {
|
|
1267
|
+
box-shadow: none !important;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/* Mobile: button press animation - depress then spring back */
|
|
1272
|
+
@media (hover: none) {
|
|
1273
|
+
.day-cell.selected {
|
|
1274
|
+
animation: button-press 0.15s ease-out !important;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.day-cell.selected .day-number {
|
|
1278
|
+
animation: text-appear 0.15s ease-out !important;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/* Deselect animation - same press effect */
|
|
1282
|
+
.day-cell.clickable:not(.selected):not(.past-day) {
|
|
1283
|
+
animation: button-depress 0.15s ease-out;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.day-cell.clickable:not(.selected):not(.past-day) .day-number {
|
|
1287
|
+
animation: text-depress 0.15s ease-out;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
@keyframes button-press {
|
|
1292
|
+
0% {
|
|
1293
|
+
transform: scale(0.88);
|
|
1294
|
+
background-color: hsl(var(--primary-700));
|
|
1295
|
+
}
|
|
1296
|
+
40% {
|
|
1297
|
+
transform: scale(0.88);
|
|
1298
|
+
background-color: hsl(var(--primary-700));
|
|
1299
|
+
}
|
|
1300
|
+
100% {
|
|
1301
|
+
transform: scale(1);
|
|
1302
|
+
background-color: hsl(var(--primary-700));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
@keyframes text-appear {
|
|
1307
|
+
0% {
|
|
1308
|
+
transform: scale(0.85);
|
|
1309
|
+
color: white;
|
|
1310
|
+
}
|
|
1311
|
+
40% {
|
|
1312
|
+
transform: scale(0.85);
|
|
1313
|
+
color: white;
|
|
1314
|
+
}
|
|
1315
|
+
100% {
|
|
1316
|
+
transform: scale(1);
|
|
1317
|
+
color: white;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
@keyframes button-depress {
|
|
1322
|
+
0% {
|
|
1323
|
+
transform: scale(0.88);
|
|
1324
|
+
background-color: hsl(var(--primary-700));
|
|
1325
|
+
}
|
|
1326
|
+
40% {
|
|
1327
|
+
transform: scale(0.88);
|
|
1328
|
+
background-color: hsl(var(--primary-500));
|
|
1329
|
+
}
|
|
1330
|
+
100% {
|
|
1331
|
+
transform: scale(1);
|
|
1332
|
+
background-color: transparent;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
@keyframes text-depress {
|
|
1337
|
+
0% {
|
|
1338
|
+
transform: scale(0.85);
|
|
1339
|
+
color: white;
|
|
1340
|
+
}
|
|
1341
|
+
40% {
|
|
1342
|
+
transform: scale(0.85);
|
|
1343
|
+
color: white;
|
|
1344
|
+
}
|
|
1345
|
+
100% {
|
|
1346
|
+
transform: scale(1);
|
|
1347
|
+
color: hsl(var(--Text-Primary));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/* Days with events (display/public variants) - mobile highlight */
|
|
1352
|
+
@media (max-width: 639px) {
|
|
1353
|
+
.day-cell.has-events {
|
|
1354
|
+
background-color: hsl(var(--Brand-Primary) / 0.12);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.day-cell.has-events:active {
|
|
1358
|
+
background-color: hsl(var(--Brand-Primary) / 0.18);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/* Event dots */
|
|
1363
|
+
.event-dots {
|
|
1364
|
+
display: flex;
|
|
1365
|
+
gap: 2px;
|
|
1366
|
+
margin-top: 2px;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.event-dot {
|
|
1370
|
+
width: 6px;
|
|
1371
|
+
height: 6px;
|
|
1372
|
+
border-radius: 50%;
|
|
1373
|
+
background: hsl(var(--Calendar-Dot, var(--Brand-Primary)));
|
|
1374
|
+
transition: transform 0.15s ease;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
@media (min-width: 640px) {
|
|
1378
|
+
.day-cell:hover .event-dot {
|
|
1379
|
+
transform: scale(1.3);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/* --------------------------------------------------------------------------
|
|
1384
|
+
LEGEND
|
|
1385
|
+
-------------------------------------------------------------------------- */
|
|
1386
|
+
.calendar-legend {
|
|
1387
|
+
display: flex;
|
|
1388
|
+
align-items: center;
|
|
1389
|
+
justify-content: center;
|
|
1390
|
+
gap: 1.5rem;
|
|
1391
|
+
padding-top: 1rem;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.legend-item {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
align-items: center;
|
|
1397
|
+
gap: 0.5rem;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.legend-dot {
|
|
1401
|
+
width: 1.25rem;
|
|
1402
|
+
height: 1.25rem;
|
|
1403
|
+
border-radius: 4px;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
.legend-dot--available {
|
|
1407
|
+
background-color: hsl(var(--primary-700));
|
|
1408
|
+
border: 1px solid hsl(var(--primary-700));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.legend-dot--unavailable {
|
|
1412
|
+
background-color: hsl(var(--BG-Secondary));
|
|
1413
|
+
border: 2px solid hsl(var(--Text-Tertiary, var(--Text-Tartiary)));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.legend-label {
|
|
1417
|
+
font-size: 0.875rem;
|
|
1418
|
+
color: hsl(var(--Text-Primary));
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/* --------------------------------------------------------------------------
|
|
1422
|
+
ANIMATION
|
|
1423
|
+
-------------------------------------------------------------------------- */
|
|
1424
|
+
.animate-spin {
|
|
1425
|
+
animation: spin 1s linear infinite;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
@keyframes spin {
|
|
1429
|
+
from { transform: rotate(0deg); }
|
|
1430
|
+
to { transform: rotate(360deg); }
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1434
|
+
.nav-btn,
|
|
1435
|
+
.today-btn,
|
|
1436
|
+
.day-cell,
|
|
1437
|
+
.day-number,
|
|
1438
|
+
.event-dot {
|
|
1439
|
+
transition: none;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.day-cell.selected {
|
|
1443
|
+
animation: none;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
</style>
|