diff --git a/public/admin.js b/public/admin.js index 5572b4c..759754b 100644 --- a/public/admin.js +++ b/public/admin.js @@ -309,217 +309,427 @@ function initializeVisualTimePicker(dateStr, availableTimes) { existingTimePicker.remove(); } - // Create the visual time picker container - const timePicker = document.createElement('div'); - timePicker.id = 'visualTimePicker'; - timePicker.classList.add('card', 'shadow'); - timePicker.style.display = 'none'; - timePicker.style.position = 'fixed'; - timePicker.style.zIndex = '1050'; - timePicker.style.width = '300px'; - timePicker.style.top = '50%'; - timePicker.style.left = '50%'; - timePicker.style.transform = 'translate(-50%, -50%)'; - - // Create the header - const header = document.createElement('div'); - header.classList.add('card-header', 'bg-primary', 'text-white', 'text-center', 'p-0'); - - // Create the time display in the header - const timeDisplay = document.createElement('div'); - timeDisplay.classList.add('display-4', 'py-3', 'mb-0'); - timeDisplay.innerHTML = '12:00 PM'; - header.appendChild(timeDisplay); - - timePicker.appendChild(header); - - // Create the body - const body = document.createElement('div'); - body.classList.add('card-body', 'bg-white'); - - // Create the clock face container - const clockFace = document.createElement('div'); - clockFace.classList.add('time-picker-clock', 'mb-3'); - clockFace.style.position = 'relative'; - clockFace.style.width = '250px'; - clockFace.style.height = '250px'; - clockFace.style.margin = '0 auto'; - clockFace.style.backgroundColor = '#f8f9fa'; - clockFace.style.borderRadius = '50%'; - - // Add clock center dot - const clockCenter = document.createElement('div'); - clockCenter.style.position = 'absolute'; - clockCenter.style.top = '50%'; - clockCenter.style.left = '50%'; - clockCenter.style.transform = 'translate(-50%, -50%)'; - clockCenter.style.width = '8px'; - clockCenter.style.height = '8px'; - clockCenter.style.backgroundColor = '#0d6efd'; - clockCenter.style.borderRadius = '50%'; - clockFace.appendChild(clockCenter); - - // Add hour numbers to the clock face - for (let i = 1; i <= 12; i++) { - const hourNumber = document.createElement('div'); - hourNumber.classList.add('hour-number'); - hourNumber.textContent = i; - hourNumber.style.position = 'absolute'; - hourNumber.style.width = '40px'; - hourNumber.style.height = '40px'; - hourNumber.style.textAlign = 'center'; - hourNumber.style.lineHeight = '40px'; - hourNumber.style.fontWeight = 'bold'; - hourNumber.style.cursor = 'pointer'; + // Create the timepicker modal from the template - moved to a separate function for clarity + document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate()); - // Calculate position (in a circle) - const angle = (i * 30 - 90) * (Math.PI / 180); // 30 degrees per hour, starting at 12 o'clock - const radius = 100; // Distance from center - const left = 125 + radius * Math.cos(angle); - const top = 125 + radius * Math.sin(angle); + // Initialize timepicker functionality and store the reference to the controller + const timepickerController = initializeTimepickerFunctionality(dateStr, availableTimes); + + // Show time picker when button is clicked + timeButton.addEventListener('click', (e) => { + showTimepicker(timepickerController); + e.stopPropagation(); + }); + + // Close time picker when clicking outside + document.addEventListener('click', (e) => { + const timepicker = document.getElementById('visualTimePicker'); + const timepickerContainer = document.querySelector('.timepicker-container'); - hourNumber.style.left = `${left - 20}px`; // Adjust for element width - hourNumber.style.top = `${top - 20}px`; // Adjust for element height + // Only close if we're not in the middle of a drag operation + if (timepicker && + timepicker.style.display === 'block' && + e.target !== timeButton && + (!timepickerContainer || !timepickerContainer.contains(e.target)) && + !window.timepickerDragging) { // Check the global dragging flag + timepickerController.closeTimepicker(); + } + }); +} + +// Helper function to show the timepicker +function showTimepicker(timepickerController) { + const timepicker = document.getElementById('visualTimePicker'); + if (!timepicker) return; + + // First make the timepicker visible but with opacity 0 + timepicker.style.display = 'block'; + timepicker.style.opacity = '0'; + + // Force a reflow to ensure the clock dimensions are calculated + setTimeout(() => { + // Always start with hours view when opening the timepicker + timepickerController.switchToHoursView(); - // Add click event to select hour - hourNumber.addEventListener('click', () => { - document.getElementById('timePickerHour').textContent = i; + // Make the timepicker visible + timepicker.style.opacity = '1'; + document.body.style.overflow = 'hidden'; + document.body.style.paddingRight = '0px'; + }, 10); +} + +// Create the timepicker HTML template +function createTimepickerTemplate() { + return ` +
`; +} + +// Initialize the timepicker functionality +function initializeTimepickerFunctionality(dateStr, availableTimes) { + // DOM elements - group related elements together + const timeElements = { + hour: document.querySelector('.timepicker-hour'), + minute: document.querySelector('.timepicker-minute'), + am: document.querySelector('.timepicker-am'), + pm: document.querySelector('.timepicker-pm'), + dot: document.querySelector('.timepicker-dot') + }; + + const clockElements = { + wrapper: document.querySelector('.timepicker-clock-wrapper'), + clock: document.querySelector('.timepicker-clock'), + hand: document.querySelector('.timepicker-hand-pointer') + }; + + const actionButtons = { + clear: document.querySelector('.timepicker-clear'), + cancel: document.querySelector('.timepicker-cancel'), + submit: document.querySelector('.timepicker-submit') + }; + + // State variables + const state = { + currentView: 'hours', + selectedHour: 12, + selectedMinute: 0, + isDragging: false, + isPM: timeElements.pm.classList.contains('active') + }; + + // Create a global variable to track if we're dragging from inside the timepicker + window.timepickerDragging = false; + + // Initialize + setupEventListeners(); + renderClockFace(state.currentView); + + function setupEventListeners() { + // Header hour/minute buttons + timeElements.hour.addEventListener('click', () => switchView('hours')); + timeElements.minute.addEventListener('click', () => switchView('minutes')); - clockFace.appendChild(hourNumber); + // Clock face events + clockElements.clock.addEventListener('mousedown', handleClockMouseDown); + document.addEventListener('mousemove', handleClockMouseMove); + document.addEventListener('mouseup', handleClockMouseUp); + + // Prevent text selection on the clock + clockElements.clock.style.userSelect = 'none'; + clockElements.clock.style.webkitUserSelect = 'none'; + clockElements.clock.style.msUserSelect = 'none'; + + // AM/PM buttons + timeElements.am.addEventListener('click', () => setAmPm('AM')); + timeElements.pm.addEventListener('click', () => setAmPm('PM')); + + // Action buttons + actionButtons.clear.addEventListener('click', clearTime); + actionButtons.cancel.addEventListener('click', closeTimepicker); + actionButtons.submit.addEventListener('click', submitTime); + } + + function switchView(view) { + state.currentView = view; + + // Update active state in header + if (view === 'hours') { + timeElements.hour.classList.add('active'); + timeElements.minute.classList.remove('active'); + timeElements.hour.style.pointerEvents = 'none'; + timeElements.minute.style.pointerEvents = 'auto'; + } else { + timeElements.hour.classList.remove('active'); + timeElements.minute.classList.add('active'); + timeElements.hour.style.pointerEvents = 'auto'; + timeElements.minute.style.pointerEvents = 'none'; + } + + renderClockFace(view); + } + + function renderClockFace(view) { + // Clear existing time tips + const existingTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes'); + existingTips.forEach(tip => tip.remove()); + + if (view === 'hours') { + renderHoursFace(); + updateHandPosition(state.selectedHour, 'hours'); + } else { + renderMinutesFace(); + updateHandPosition(state.selectedMinute, 'minutes'); + } + } + + function renderHoursFace() { + const clockRadius = clockElements.clock.offsetWidth / 2; + const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) + + for (let hour = 1; hour <= 12; hour++) { + const tip = createClockTip(hour, clockRadius, tipRadius, 'hours'); + + if (hour === state.selectedHour) { + tip.classList.add('active'); + } + + clockElements.clock.appendChild(tip); + } + } + + function renderMinutesFace() { + const clockRadius = clockElements.clock.offsetWidth / 2; + const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) + + for (let minute = 0; minute < 60; minute += 5) { + const tip = createClockTip(minute, clockRadius, tipRadius, 'minutes'); + + if (minute === state.selectedMinute) { + tip.classList.add('active'); + } + + clockElements.clock.appendChild(tip); + } } - // Function to show minute selection - function showMinuteSelection() { - // Hide hour numbers - document.querySelectorAll('.hour-number').forEach(el => { - el.style.display = 'none'; - }); + // Helper function to create clock face tips (numbers) + function createClockTip(value, clockRadius, tipRadius, type) { + // Calculate angle and position + let angle, displayValue; - // Show minute numbers - document.querySelectorAll('.minute-number').forEach(el => { - el.style.display = 'block'; + if (type === 'hours') { + angle = ((value * 30) - 90) * (Math.PI / 180); + displayValue = value; + } else { + angle = ((value * 6) - 90) * (Math.PI / 180); + displayValue = value.toString().padStart(2, '0'); + } + + // Calculate position + const left = clockRadius + tipRadius * Math.cos(angle); + const top = clockRadius + tipRadius * Math.sin(angle); + + // Create the tip element + const tip = document.createElement('span'); + tip.className = `timepicker-time-tips-${type}`; + tip.style.left = `${left}px`; + tip.style.top = `${top}px`; + tip.style.position = 'absolute'; + tip.style.transform = 'translate(-50%, -50%)'; + tip.style.userSelect = 'none'; + tip.style.webkitUserSelect = 'none'; + tip.style.msUserSelect = 'none'; + + const tipElement = document.createElement('span'); + tipElement.className = 'timepicker-tips-element'; + tipElement.textContent = displayValue; + + tip.appendChild(tipElement); + return tip; + } + + function updateHandPosition(value, view) { + let angle; + + if (view === 'hours') { + // For hours, convert 12 to 0 for calculation purposes + const hour = value === 12 ? 0 : value; + angle = (hour / 12) * 360; + } else { + angle = (value / 60) * 360; + } + + clockElements.hand.style.transform = `rotateZ(${angle}deg)`; + + // Update the active circle on the hand + const circle = clockElements.hand.querySelector('.timepicker-circle'); + if (circle) { + circle.classList.add('active'); + } + } + + function handleClockMouseDown(event) { + state.isDragging = true; + // Set the global flag to indicate we're dragging from inside the timepicker + window.timepickerDragging = true; + updateTimeFromClockPosition(event); + } + + function handleClockMouseMove(event) { + if (state.isDragging) { + updateTimeFromClockPosition(event); + } + } + + function handleClockMouseUp() { + if (state.isDragging) { + state.isDragging = false; + + // Reset the global dragging flag after a short delay + // This allows the click event to process first + setTimeout(() => { + window.timepickerDragging = false; + }, 10); + + // If we just finished selecting an hour, switch to minutes + if (state.currentView === 'hours') { + switchView('minutes'); + } + } + } + + function updateTimeFromClockPosition(event) { + const clockRect = clockElements.clock.getBoundingClientRect(); + const centerX = clockRect.left + clockRect.width / 2; + const centerY = clockRect.top + clockRect.height / 2; + + // Calculate angle from center to mouse position + const x = event.clientX - centerX; + const y = event.clientY - centerY; + + // Calculate angle in degrees, starting from 12 o'clock position + let angle = Math.atan2(y, x) * (180 / Math.PI) + 90; + + // Normalize angle to 0-360 + if (angle < 0) { + angle += 360; + } + + if (state.currentView === 'hours') { + updateHourFromAngle(angle); + } else { + updateMinuteFromAngle(angle); + } + } + + function updateHourFromAngle(angle) { + // Convert angle to hour (1-12) + let hour = Math.round(angle / 30); + + // Handle edge cases for full circle + if (hour === 0 || hour > 12) hour = 12; + + state.selectedHour = hour; + timeElements.hour.textContent = hour; + updateHandPosition(hour, 'hours'); + + // Update active class on hour tips + const hourTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours'); + hourTips.forEach(tip => { + const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); + if (hourValue === state.selectedHour) { + tip.classList.add('active'); + } else { + tip.classList.remove('active'); + } }); } - // Function to show hour selection - function showHourSelection() { - // Hide minute numbers - document.querySelectorAll('.minute-number').forEach(el => { - el.style.display = 'none'; - }); + function updateMinuteFromAngle(angle) { + // Convert angle to minute (0-55, step 5) + let minute = Math.floor(angle / 6); - // Show hour numbers - document.querySelectorAll('.hour-number').forEach(el => { - el.style.display = 'block'; + // Round to nearest 5 + minute = Math.round(minute / 5) * 5; + + // Handle edge case for full circle + if (minute >= 60) minute = 0; + + state.selectedMinute = minute; + timeElements.minute.textContent = minute.toString().padStart(2, '0'); + updateHandPosition(minute, 'minutes'); + + // Update active class on minute tips + const minuteTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-minutes'); + minuteTips.forEach(tip => { + const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); + if (minuteValue === state.selectedMinute) { + tip.classList.add('active'); + } else { + tip.classList.remove('active'); + } }); } - - // Add minute numbers to the clock face (initially hidden) - const minuteValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; - minuteValues.forEach((minute, index) => { - const minuteNumber = document.createElement('div'); - minuteNumber.classList.add('minute-number'); - minuteNumber.textContent = minute.toString().padStart(2, '0'); - minuteNumber.style.position = 'absolute'; - minuteNumber.style.width = '40px'; - minuteNumber.style.height = '40px'; - minuteNumber.style.textAlign = 'center'; - minuteNumber.style.lineHeight = '40px'; - minuteNumber.style.fontWeight = 'bold'; - minuteNumber.style.cursor = 'pointer'; - minuteNumber.style.display = 'none'; // Initially hidden + + function setAmPm(period) { + if (period === 'AM') { + timeElements.am.classList.add('active'); + timeElements.pm.classList.remove('active'); + state.isPM = false; + } else { + timeElements.am.classList.remove('active'); + timeElements.pm.classList.add('active'); + state.isPM = true; + } + } + + function clearTime() { + state.selectedHour = 12; + state.selectedMinute = 0; + timeElements.hour.textContent = '12'; + timeElements.minute.textContent = '00'; + setAmPm('PM'); + switchView('hours'); + } + + function closeTimepicker() { + const timepicker = document.getElementById('visualTimePicker'); + if (timepicker) { + timepicker.style.display = 'none'; + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + } + } + + function submitTime() { + const formattedHour = state.selectedHour; + const formattedMinute = state.selectedMinute.toString().padStart(2, '0'); + const period = state.isPM ? 'PM' : 'AM'; - // Calculate position (in a circle) - const angle = (index * 30 - 90) * (Math.PI / 180); - const radius = 100; // Distance from center - const left = 125 + radius * Math.cos(angle); - const top = 125 + radius * Math.sin(angle); - - minuteNumber.style.left = `${left - 20}px`; // Adjust for element width - minuteNumber.style.top = `${top - 20}px`; // Adjust for element height - - // Add click event to select minute - minuteNumber.addEventListener('click', () => { - document.getElementById('timePickerMinute').textContent = minute.toString().padStart(2, '0'); - - // Highlight selected minute - document.querySelectorAll('.minute-number').forEach(el => { - el.style.backgroundColor = 'transparent'; - el.style.color = '#212529'; - }); - minuteNumber.style.backgroundColor = '#0d6efd'; - minuteNumber.style.color = 'white'; - - // Show hour selection again - showHourSelection(); - }); - - clockFace.appendChild(minuteNumber); - }); - - body.appendChild(clockFace); - - // Create AM/PM toggle - const ampmToggle = document.createElement('div'); - ampmToggle.classList.add('d-flex', 'justify-content-center', 'mb-3'); - - const amButton = document.createElement('button'); - amButton.classList.add('btn', 'btn-outline-primary', 'me-2'); - amButton.textContent = 'AM'; - amButton.addEventListener('click', () => { - document.getElementById('timePickerAMPM').textContent = 'AM'; - amButton.classList.add('active'); - pmButton.classList.remove('active'); - }); - - const pmButton = document.createElement('button'); - pmButton.classList.add('btn', 'btn-outline-primary'); - pmButton.textContent = 'PM'; - pmButton.addEventListener('click', () => { - document.getElementById('timePickerAMPM').textContent = 'PM'; - pmButton.classList.add('active'); - amButton.classList.remove('active'); - }); - pmButton.classList.add('active'); // Add active class to PM button - - ampmToggle.appendChild(amButton); - ampmToggle.appendChild(pmButton); - - body.appendChild(ampmToggle); - - // Create action buttons - const actionButtons = document.createElement('div'); - actionButtons.classList.add('d-flex', 'justify-content-between'); - - const closeButton = document.createElement('button'); - closeButton.classList.add('btn', 'btn-outline-secondary', 'flex-grow-1', 'me-2'); - closeButton.textContent = 'CANCEL'; - closeButton.addEventListener('click', () => { - timePicker.style.display = 'none'; - }); - - const okButton = document.createElement('button'); - okButton.classList.add('btn', 'btn-primary', 'flex-grow-1'); - okButton.textContent = 'OK'; - okButton.addEventListener('click', () => { - const hour = document.getElementById('timePickerHour').textContent; - const minute = document.getElementById('timePickerMinute').textContent; - const ampm = document.getElementById('timePickerAMPM').textContent.toLowerCase(); - - const timeValue = `${hour}:${minute}${ampm}`; - document.getElementById('customTimeInput').value = timeValue; + const timeValue = `${formattedHour}:${formattedMinute}${period.toLowerCase()}`; // Check if time already exists in available times if (availableTimes.includes(timeValue)) { @@ -530,37 +740,15 @@ function initializeVisualTimePicker(dateStr, availableTimes) { addSingleTime(dateStr, timeValue); } - // Hide the time picker - timePicker.style.display = 'none'; - }); - - actionButtons.appendChild(closeButton); - actionButtons.appendChild(okButton); - - body.appendChild(actionButtons); - - timePicker.appendChild(body); - - // Add the time picker to the document - document.body.appendChild(timePicker); - - // Show time picker when button is clicked - timeButton.addEventListener('click', (e) => { - // Remove positioning relative to button - timePicker.style.display = 'block'; - - // Reset to default view (hour selection) - showHourSelection(); - - e.stopPropagation(); - }); - - // Close time picker when clicking outside - document.addEventListener('click', (e) => { - if (e.target !== timeButton && !timePicker.contains(e.target)) { - timePicker.style.display = 'none'; - } - }); + closeTimepicker(); + } + + // Return controller object with public methods + return { + renderClockFace, + closeTimepicker, + switchToHoursView: () => switchView('hours') + }; } // Fetch available dates from the API diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 0519ecb..0000000 --- a/public/index.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/timepicker.css b/public/timepicker.css new file mode 100644 index 0000000..1e9af76 --- /dev/null +++ b/public/timepicker.css @@ -0,0 +1,413 @@ +/** + * Timepicker styling + */ +/* Core timepicker styling */ +.timepicker-wrapper { + --fs-timepicker-wrapper-bg: rgba(0, 0, 0, 0.4); + --fs-timepicker-elements-min-width: 310px; + --fs-timepicker-elements-min-height: 325px; + --fs-timepicker-elements-background: #fff; + --fs-timepicker-elements-border-top-right-radius: 0.6rem; + --fs-timepicker-elements-border-top-left-radius: 0.6rem; + --fs-timepicker-head-bg: #3b71ca; + --fs-timepicker-head-height: 100px; + --fs-timepicker-head-border-top-right-radius: 0.5rem; + --fs-timepicker-head-border-top-left-radius: 0.5rem; + --fs-timepicker-head-padding-y: 10px; + --fs-timepicker-head-padding-right: 24px; + --fs-timepicker-head-padding-left: 50px; + --fs-timepicker-button-font-size: 0.8rem; + --fs-timepicker-button-min-width: 64px; + --fs-timepicker-button-font-weight: 500; + --fs-timepicker-button-line-height: 40px; + --fs-timepicker-button-border-radius: 10px; + --fs-timepicker-button-color: #4f4f4f; + --fs-timepicker-button-hover-bg: rgba(0, 0, 0, 0.08); + --fs-timepicker-button-focus-bg: rgba(0, 0, 0, 0.08); + --fs-timepicker-button-padding-x: 10px; + --fs-timepicker-button-height: 40px; + --fs-timepicker-button-margin-bottom: 10px; + --fs-timepicker-current-font-size: 3.75rem; + --fs-timepicker-current-font-weight: 300; + --fs-timepicker-current-line-height: 1.2; + --fs-timepicker-current-color: #fff; + --fs-timepicker-current-opacity: 0.54; + --fs-timepicker-clock-wrapper-min-width: 310px; + --fs-timepicker-clock-wrapper-max-width: 325px; + --fs-timepicker-clock-wrapper-min-height: 305px; + --fs-timepicker-clock-wrapper-text-color: #4f4f4f; + --fs-timepicker-mode-wrapper-font-size: 18px; + --fs-timepicker-mode-wrapper-color: rgba(255, 255, 255, 0.54); + --fs-timepicker-clock-width: 260px; + --fs-timepicker-clock-height: 260px; + --fs-timepicker-clock-face-bg: #f0f0f0; + --fs-timepicker-time-tips-inner-active-color: #fff; + --fs-timepicker-time-tips-inner-active-bg: #3b71ca; + --fs-timepicker-time-tips-inner-active-font-weight: 400; + --fs-timepicker-dot-font-weight: 300; + --fs-timepicker-dot-line-height: 1.2; + --fs-timepicker-dot-color: #fff; + --fs-timepicker-dot-font-size: 3.75rem; + --fs-timepicker-dot-opacity: 0.54; + --fs-timepicker-item-middle-dot-width: 6px; + --fs-timepicker-item-middle-dot-height: 6px; + --fs-timepicker-item-middle-dot-border-radius: 50%; + --fs-timepicker-item-middle-dot-bg: #3b71ca; + --fs-timepicker-hand-pointer-bg: #3b71ca; + --fs-timepicker-hand-pointer-bottom: 50%; + --fs-timepicker-hand-pointer-height: 40%; + --fs-timepicker-hand-pointer-left: calc(50% - 1px); + --fs-timepicker-hand-pointer-width: 2px; + --fs-timepicker-circle-top: -21px; + --fs-timepicker-circle-left: -15px; + --fs-timepicker-circle-width: 4px; + --fs-timepicker-circle-border-width: 14px; + --fs-timepicker-circle-border-color: #3b71ca; + --fs-timepicker-circle-height: 4px; + --fs-timepicker-circle-active-background-color: #fff; + --fs-timepicker-hour-mode-color: #fff; + --fs-timepicker-hour-mode-opacity: 0.54; + --fs-timepicker-hour-mode-hover-bg: rgba(0, 0, 0, 0.15); + --fs-timepicker-hour-mode-active-color: #fff; + --fs-timepicker-footer-border-bottom-left-radius: 0.5rem; + --fs-timepicker-footer-border-bottom-right-radius: 0.5rem; + --fs-timepicker-footer-height: 56px; + --fs-timepicker-footer-padding-x: 12px; + --fs-timepicker-footer-bg: #fff; + --fs-timepicker-clock-animation: show-up-clock 350ms linear; + --fs-timepicker-zindex: 1065; + + touch-action: none; + z-index: var(--fs-timepicker-zindex); + opacity: 1; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: var(--fs-timepicker-wrapper-bg); +} + +/* Animation */ +.animation { + animation-fill-mode: both; +} + +.fade-in { + animation-name: fadeIn; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes show-up-clock { + 0% { + opacity: 0; + transform: scale(0.7); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Timepicker components */ +.timepicker-modal { + margin: 0; + font-family: "Roboto", sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: #4f4f4f; + text-align: left; + background-color: #fff; + z-index: var(--fs-timepicker-zindex); + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +/* Ensure all elements inside the timepicker use border-box */ +.timepicker-modal *, +.timepicker-modal *::before, +.timepicker-modal *::after { + box-sizing: border-box; +} + +.timepicker-container { + max-height: calc(100% - 64px); + overflow-y: auto; + box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04); +} + +.timepicker-elements { + min-width: var(--fs-timepicker-elements-min-width); + min-height: var(--fs-timepicker-elements-min-height); + background: var(--fs-timepicker-elements-background); + border-top-right-radius: var(--fs-timepicker-elements-border-top-right-radius); + border-top-left-radius: var(--fs-timepicker-elements-border-top-left-radius); +} + +.timepicker-head { + background-color: var(--fs-timepicker-head-bg); + height: var(--fs-timepicker-head-height); + border-top-right-radius: var(--fs-timepicker-head-border-top-right-radius); + border-top-left-radius: var(--fs-timepicker-head-border-top-left-radius); + padding: var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-right) var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-left); +} + +.timepicker-current { + font-size: var(--fs-timepicker-current-font-size); + font-weight: var(--fs-timepicker-current-font-weight); + line-height: var(--fs-timepicker-current-line-height); + color: var(--fs-timepicker-current-color); + opacity: var(--fs-timepicker-current-opacity); + border: none; + background: transparent; + padding: 0; + position: relative; + vertical-align: unset; +} + +.timepicker-current.active { + opacity: 1; +} + +.timepicker-dot { + font-size: var(--fs-timepicker-dot-font-size); + font-weight: var(--fs-timepicker-dot-font-weight); + line-height: var(--fs-timepicker-dot-line-height); + color: var(--fs-timepicker-dot-color); + opacity: var(--fs-timepicker-dot-opacity); + border: none; + background: transparent; + padding: 0; +} + +.timepicker-mode-wrapper { + font-size: var(--fs-timepicker-mode-wrapper-font-size); + color: var(--fs-timepicker-mode-wrapper-color); +} + +.timepicker-hour-mode { + padding: 0; + background-color: transparent; + border: none; + color: var(--fs-timepicker-hour-mode-color); + opacity: var(--fs-timepicker-hour-mode-opacity); + cursor: pointer; +} + +/* These focus styles are overridden later in the file with !important */ +.timepicker-hour-mode:hover, +.timepicker-hour-mode:focus, +.timepicker-hour:hover, +.timepicker-hour:focus, +.timepicker-minute:hover, +.timepicker-minute:focus { + background-color: var(--fs-timepicker-hour-mode-hover-bg); + outline: none; + cursor: pointer; +} + +.timepicker-hour-mode.active, +.timepicker-hour.active, +.timepicker-minute.active { + color: #fff; + opacity: 1; + outline: none; +} + +.timepicker-clock-wrapper { + min-width: var(--fs-timepicker-clock-wrapper-min-width); + max-width: var(--fs-timepicker-clock-wrapper-max-width); + min-height: var(--fs-timepicker-clock-wrapper-min-height); + overflow-x: hidden; + height: 100%; + color: var(--fs-timepicker-clock-wrapper-text-color); +} + +.timepicker-clock { + position: relative; + border-radius: 100%; + width: var(--fs-timepicker-clock-width); + height: var(--fs-timepicker-clock-height); + cursor: default; + margin: 0 auto; + background-color: var(--fs-timepicker-clock-face-bg); + user-select: none; +} + +.timepicker-clock-animation { + animation: var(--fs-timepicker-clock-animation); +} + +.timepicker-middle-dot { + top: 50%; + left: 50%; + width: var(--fs-timepicker-item-middle-dot-width); + height: var(--fs-timepicker-item-middle-dot-height); + transform: translate(-50%, -50%); + border-radius: var(--fs-timepicker-item-middle-dot-border-radius); + background-color: var(--fs-timepicker-item-middle-dot-bg); +} + +.timepicker-hand-pointer { + background-color: var(--fs-timepicker-hand-pointer-bg); + bottom: var(--fs-timepicker-hand-pointer-bottom); + height: var(--fs-timepicker-hand-pointer-height); + left: var(--fs-timepicker-hand-pointer-left); + transform-origin: center bottom 0; + width: var(--fs-timepicker-hand-pointer-width); +} + +.timepicker-circle { + top: var(--fs-timepicker-circle-top); + left: var(--fs-timepicker-circle-left); + width: var(--fs-timepicker-circle-width); + border: var(--fs-timepicker-circle-border-width) solid var(--fs-timepicker-circle-border-color); + height: var(--fs-timepicker-circle-height); + box-sizing: content-box; + border-radius: 100%; + background-color: transparent; +} + +.timepicker-circle.active { + background-color: var(--fs-timepicker-circle-active-background-color); +} + +.timepicker-time-tips-minutes, +.timepicker-time-tips-hours { + position: absolute; + border-radius: 100%; + width: 32px; + height: 32px; + text-align: center; + cursor: pointer; + font-size: 1.1rem; + display: flex; + justify-content: center; + align-items: center; + font-weight: 300; + user-select: none; +} + +.timepicker-time-tips-minutes.active, +.timepicker-time-tips-hours.active { + color: var(--fs-timepicker-time-tips-inner-active-color); + background-color: var(--fs-timepicker-time-tips-inner-active-bg); + font-weight: var(--fs-timepicker-time-tips-inner-active-font-weight); +} + +.timepicker-footer { + border-bottom-left-radius: var(--fs-timepicker-footer-border-bottom-left-radius); + border-bottom-right-radius: var(--fs-timepicker-footer-border-bottom-right-radius); + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: var(--fs-timepicker-footer-height); + padding-left: var(--fs-timepicker-footer-padding-x); + padding-right: var(--fs-timepicker-footer-padding-x); + background-color: var(--fs-timepicker-footer-bg); +} + +.timepicker-button { + font-size: var(--fs-timepicker-button-font-size); + min-width: var(--fs-timepicker-button-min-width); + box-sizing: border-box; + font-weight: var(--fs-timepicker-button-font-weight); + line-height: var(--fs-timepicker-button-line-height); + border-radius: var(--fs-timepicker-button-border-radius); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fs-timepicker-button-color); + border: none; + background-color: transparent; + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + outline: none; + padding: 0 var(--fs-timepicker-button-padding-x); + height: var(--fs-timepicker-button-height); + margin-bottom: var(--fs-timepicker-button-margin-bottom); + cursor: pointer; +} + +.timepicker-button:hover { + background-color: var(--fs-timepicker-button-hover-bg); +} + +.timepicker-button:focus { + outline: none; + background-color: var(--fs-timepicker-button-focus-bg); +} + +/* Add missing button styling */ +button { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +/** Remove focus from buttons after clicked **/ +/* Prevent selection/focus styling on all buttons in the timepicker */ +.timepicker-modal button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} + +/* Override focus styles completely */ +.timepicker-modal button:focus { + outline: none !important; + box-shadow: none !important; + -webkit-box-shadow: none !important; +} + +/* Prevent text selection on all elements */ +.timepicker-modal * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* Add this to the body when the timepicker is active */ +body.timepicker-active { + -webkit-tap-highlight-color: transparent; +} + +/* Add !important to ensure these styles take precedence */ +.timepicker-current:focus, +.timepicker-dot:focus, +.timepicker-hour-mode:focus, +.timepicker-button:focus { + outline: none !important; + background-color: transparent !important; +} + +/* Only apply background color on hover, not on focus */ +.timepicker-hour-mode:hover, +.timepicker-hour:hover, +.timepicker-minute:hover, +.timepicker-button:hover { + background-color: var(--fs-timepicker-hour-mode-hover-bg); +} + +/* Ensure active states are properly styled */ +.timepicker-hour-mode.active, +.timepicker-hour.active, +.timepicker-minute.active { + color: #fff !important; + opacity: 1 !important; +} \ No newline at end of file diff --git a/timepicker-modal/timepicker-modal.css b/timepicker-modal/timepicker-modal.css new file mode 100644 index 0000000..157f6ea --- /dev/null +++ b/timepicker-modal/timepicker-modal.css @@ -0,0 +1,468 @@ +/* Core timepicker styling */ +.timepicker-wrapper { + --fs-timepicker-wrapper-bg: rgba(0, 0, 0, 0.4); + --fs-timepicker-elements-min-width: 310px; + --fs-timepicker-elements-min-height: 325px; + --fs-timepicker-elements-background: #fff; + --fs-timepicker-elements-border-top-right-radius: 0.6rem; + --fs-timepicker-elements-border-top-left-radius: 0.6rem; + --fs-timepicker-head-bg: #3b71ca; + --fs-timepicker-head-height: 100px; + --fs-timepicker-head-border-top-right-radius: 0.5rem; + --fs-timepicker-head-border-top-left-radius: 0.5rem; + --fs-timepicker-head-padding-y: 10px; + --fs-timepicker-head-padding-right: 24px; + --fs-timepicker-head-padding-left: 50px; + --fs-timepicker-button-font-size: 0.8rem; + --fs-timepicker-button-min-width: 64px; + --fs-timepicker-button-font-weight: 500; + --fs-timepicker-button-line-height: 40px; + --fs-timepicker-button-border-radius: 10px; + --fs-timepicker-button-color: #4f4f4f; + --fs-timepicker-button-hover-bg: rgba(0, 0, 0, 0.08); + --fs-timepicker-button-focus-bg: rgba(0, 0, 0, 0.08); + --fs-timepicker-button-padding-x: 10px; + --fs-timepicker-button-height: 40px; + --fs-timepicker-button-margin-bottom: 10px; + --fs-timepicker-current-font-size: 3.75rem; + --fs-timepicker-current-font-weight: 300; + --fs-timepicker-current-line-height: 1.2; + --fs-timepicker-current-color: #fff; + --fs-timepicker-current-opacity: 0.54; + --fs-timepicker-clock-wrapper-min-width: 310px; + --fs-timepicker-clock-wrapper-max-width: 325px; + --fs-timepicker-clock-wrapper-min-height: 305px; + --fs-timepicker-clock-wrapper-text-color: #4f4f4f; + --fs-timepicker-mode-wrapper-font-size: 18px; + --fs-timepicker-mode-wrapper-color: rgba(255, 255, 255, 0.54); + --fs-timepicker-clock-width: 260px; + --fs-timepicker-clock-height: 260px; + --fs-timepicker-clock-face-bg: #f0f0f0; + --fs-timepicker-time-tips-inner-active-color: #fff; + --fs-timepicker-time-tips-inner-active-bg: #3b71ca; + --fs-timepicker-time-tips-inner-active-font-weight: 400; + --fs-timepicker-dot-font-weight: 300; + --fs-timepicker-dot-line-height: 1.2; + --fs-timepicker-dot-color: #fff; + --fs-timepicker-dot-font-size: 3.75rem; + --fs-timepicker-dot-opacity: 0.54; + --fs-timepicker-item-middle-dot-width: 6px; + --fs-timepicker-item-middle-dot-height: 6px; + --fs-timepicker-item-middle-dot-border-radius: 50%; + --fs-timepicker-item-middle-dot-bg: #3b71ca; + --fs-timepicker-hand-pointer-bg: #3b71ca; + --fs-timepicker-hand-pointer-bottom: 50%; + --fs-timepicker-hand-pointer-height: 40%; + --fs-timepicker-hand-pointer-left: calc(50% - 1px); + --fs-timepicker-hand-pointer-width: 2px; + --fs-timepicker-circle-top: -21px; + --fs-timepicker-circle-left: -15px; + --fs-timepicker-circle-width: 4px; + --fs-timepicker-circle-border-width: 14px; + --fs-timepicker-circle-border-color: #3b71ca; + --fs-timepicker-circle-height: 4px; + --fs-timepicker-circle-active-background-color: #fff; + --fs-timepicker-hour-mode-color: #fff; + --fs-timepicker-hour-mode-opacity: 0.54; + --fs-timepicker-hour-mode-hover-bg: rgba(0, 0, 0, 0.15); + --fs-timepicker-hour-mode-active-color: #fff; + --fs-timepicker-footer-border-bottom-left-radius: 0.5rem; + --fs-timepicker-footer-border-bottom-right-radius: 0.5rem; + --fs-timepicker-footer-height: 56px; + --fs-timepicker-footer-padding-x: 12px; + --fs-timepicker-footer-bg: #fff; + --fs-timepicker-clock-animation: show-up-clock 350ms linear; + --fs-timepicker-zindex: 1065; + + touch-action: none; + z-index: var(--fs-timepicker-zindex); + opacity: 1; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: var(--fs-timepicker-wrapper-bg); +} + +/* Layout classes */ +.h-100 { + height: 100%; +} + +.d-flex { + display: flex; +} + +.flex-column { + flex-direction: column; +} + +.flex-row { + flex-direction: row; +} + +.align-items-center { + align-items: center; +} + +.justify-content-center { + justify-content: center; +} + +.justify-content-between { + justify-content: space-between; +} + +.justify-content-around { + justify-content: space-around; +} + +.justify-content-evenly { + justify-content: space-evenly; +} + +.w-100 { + width: 100%; +} + +.position-fixed { + position: fixed; +} + +.position-absolute { + position: absolute; +} + +.position-relative { + position: relative; +} + +/* Animation */ +.animation { + animation-fill-mode: both; +} + +.fade-in { + animation-name: fadeIn; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes show-up-clock { + 0% { + opacity: 0; + transform: scale(0.7); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Base typography and inherited properties */ +body { + margin: 0; + font-family: "Roboto", sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: #4f4f4f; + text-align: left; + background-color: #fff; +} + +/* Timepicker components */ +.timepicker-modal { + z-index: var(--fs-timepicker-zindex); + font-family: "Roboto", sans-serif; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +/* Ensure all elements inside the timepicker use border-box */ +.timepicker-modal *, +.timepicker-modal *::before, +.timepicker-modal *::after { + box-sizing: border-box; +} + +.timepicker-container { + max-height: calc(100% - 64px); + overflow-y: auto; + box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04); +} + +.timepicker-elements { + min-width: var(--fs-timepicker-elements-min-width); + min-height: var(--fs-timepicker-elements-min-height); + background: var(--fs-timepicker-elements-background); + border-top-right-radius: var(--fs-timepicker-elements-border-top-right-radius); + border-top-left-radius: var(--fs-timepicker-elements-border-top-left-radius); +} + +.timepicker-head { + background-color: var(--fs-timepicker-head-bg); + height: var(--fs-timepicker-head-height); + border-top-right-radius: var(--fs-timepicker-head-border-top-right-radius); + border-top-left-radius: var(--fs-timepicker-head-border-top-left-radius); + padding: var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-right) var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-left); +} + +.timepicker-current { + font-size: var(--fs-timepicker-current-font-size); + font-weight: var(--fs-timepicker-current-font-weight); + line-height: var(--fs-timepicker-current-line-height); + color: var(--fs-timepicker-current-color); + opacity: var(--fs-timepicker-current-opacity); + border: none; + background: transparent; + padding: 0; + position: relative; + vertical-align: unset; +} + +.timepicker-current.active { + opacity: 1; +} + +.timepicker-dot { + font-size: var(--fs-timepicker-dot-font-size); + font-weight: var(--fs-timepicker-dot-font-weight); + line-height: var(--fs-timepicker-dot-line-height); + color: var(--fs-timepicker-dot-color); + opacity: var(--fs-timepicker-dot-opacity); + border: none; + background: transparent; + padding: 0; +} + +.timepicker-mode-wrapper { + font-size: var(--fs-timepicker-mode-wrapper-font-size); + color: var(--fs-timepicker-mode-wrapper-color); +} + +.timepicker-hour-mode { + padding: 0; + background-color: transparent; + border: none; + color: var(--fs-timepicker-hour-mode-color); + opacity: var(--fs-timepicker-hour-mode-opacity); + cursor: pointer; +} + +/* These focus styles are overridden later in the file with !important */ +.timepicker-hour-mode:hover, +.timepicker-hour-mode:focus, +.timepicker-hour:hover, +.timepicker-hour:focus, +.timepicker-minute:hover, +.timepicker-minute:focus { + background-color: var(--fs-timepicker-hour-mode-hover-bg); + outline: none; + cursor: pointer; +} + +.timepicker-hour-mode.active, +.timepicker-hour.active, +.timepicker-minute.active { + color: #fff; + opacity: 1; + outline: none; +} + +.timepicker-clock-wrapper { + min-width: var(--fs-timepicker-clock-wrapper-min-width); + max-width: var(--fs-timepicker-clock-wrapper-max-width); + min-height: var(--fs-timepicker-clock-wrapper-min-height); + overflow-x: hidden; + height: 100%; + color: var(--fs-timepicker-clock-wrapper-text-color); +} + +.timepicker-clock { + position: relative; + border-radius: 100%; + width: var(--fs-timepicker-clock-width); + height: var(--fs-timepicker-clock-height); + cursor: default; + margin: 0 auto; + background-color: var(--fs-timepicker-clock-face-bg); + user-select: none; +} + +.timepicker-clock-animation { + animation: var(--fs-timepicker-clock-animation); +} + +.timepicker-middle-dot { + top: 50%; + left: 50%; + width: var(--fs-timepicker-item-middle-dot-width); + height: var(--fs-timepicker-item-middle-dot-height); + transform: translate(-50%, -50%); + border-radius: var(--fs-timepicker-item-middle-dot-border-radius); + background-color: var(--fs-timepicker-item-middle-dot-bg); +} + +.timepicker-hand-pointer { + background-color: var(--fs-timepicker-hand-pointer-bg); + bottom: var(--fs-timepicker-hand-pointer-bottom); + height: var(--fs-timepicker-hand-pointer-height); + left: var(--fs-timepicker-hand-pointer-left); + transform-origin: center bottom 0; + width: var(--fs-timepicker-hand-pointer-width); +} + +.timepicker-circle { + top: var(--fs-timepicker-circle-top); + left: var(--fs-timepicker-circle-left); + width: var(--fs-timepicker-circle-width); + border: var(--fs-timepicker-circle-border-width) solid var(--fs-timepicker-circle-border-color); + height: var(--fs-timepicker-circle-height); + box-sizing: content-box; + border-radius: 100%; + background-color: transparent; +} + +.timepicker-circle.active { + background-color: var(--fs-timepicker-circle-active-background-color); +} + +.timepicker-time-tips-minutes, +.timepicker-time-tips-hours { + position: absolute; + border-radius: 100%; + width: 32px; + height: 32px; + text-align: center; + cursor: pointer; + font-size: 1.1rem; + display: flex; + justify-content: center; + align-items: center; + font-weight: 300; + user-select: none; +} + +.timepicker-time-tips-minutes.active, +.timepicker-time-tips-hours.active { + color: var(--fs-timepicker-time-tips-inner-active-color); + background-color: var(--fs-timepicker-time-tips-inner-active-bg); + font-weight: var(--fs-timepicker-time-tips-inner-active-font-weight); +} + +.timepicker-footer { + border-bottom-left-radius: var(--fs-timepicker-footer-border-bottom-left-radius); + border-bottom-right-radius: var(--fs-timepicker-footer-border-bottom-right-radius); + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: var(--fs-timepicker-footer-height); + padding-left: var(--fs-timepicker-footer-padding-x); + padding-right: var(--fs-timepicker-footer-padding-x); + background-color: var(--fs-timepicker-footer-bg); +} + +.timepicker-button { + font-size: var(--fs-timepicker-button-font-size); + min-width: var(--fs-timepicker-button-min-width); + box-sizing: border-box; + font-weight: var(--fs-timepicker-button-font-weight); + line-height: var(--fs-timepicker-button-line-height); + border-radius: var(--fs-timepicker-button-border-radius); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fs-timepicker-button-color); + border: none; + background-color: transparent; + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + outline: none; + padding: 0 var(--fs-timepicker-button-padding-x); + height: var(--fs-timepicker-button-height); + margin-bottom: var(--fs-timepicker-button-margin-bottom); + cursor: pointer; +} + +.timepicker-button:hover { + background-color: var(--fs-timepicker-button-hover-bg); +} + +.timepicker-button:focus { + outline: none; + background-color: var(--fs-timepicker-button-focus-bg); +} + +/* Add missing button styling */ +button { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +/** Remove focus from buttons after clicked **/ +/* Prevent selection/focus styling on all buttons in the timepicker */ +.timepicker-modal button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} + +/* Override focus styles completely */ +.timepicker-modal button:focus { + outline: none !important; + box-shadow: none !important; + -webkit-box-shadow: none !important; +} + +/* Prevent text selection on all elements */ +.timepicker-modal * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* Add this to the body when the timepicker is active */ +body.timepicker-active { + -webkit-tap-highlight-color: transparent; +} + +/* Add !important to ensure these styles take precedence */ +.timepicker-current:focus, +.timepicker-dot:focus, +.timepicker-hour-mode:focus, +.timepicker-button:focus { + outline: none !important; + background-color: transparent !important; +} + +/* Only apply background color on hover, not on focus */ +.timepicker-hour-mode:hover, +.timepicker-hour:hover, +.timepicker-minute:hover, +.timepicker-button:hover { + background-color: var(--fs-timepicker-hour-mode-hover-bg); +} + +/* Ensure active states are properly styled */ +.timepicker-hour-mode.active, +.timepicker-hour.active, +.timepicker-minute.active { + color: #fff !important; + opacity: 1 !important; +} \ No newline at end of file diff --git a/timepicker-modal/timepicker-modal.html b/timepicker-modal/timepicker-modal.html new file mode 100644 index 0000000..818af67 --- /dev/null +++ b/timepicker-modal/timepicker-modal.html @@ -0,0 +1,84 @@ + + +