diff --git a/docker/index.html b/docker/index.html index bf0c76c..07c8eed 100644 --- a/docker/index.html +++ b/docker/index.html @@ -739,11 +739,15 @@ body { background: var(--primary-light); } .meal-group-items { - display: none; + max-height: 0; + opacity: 0; + overflow: hidden; border-top: 1px solid var(--border); + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; } .meal-group-header.expanded + .meal-group-items { - display: block; + max-height: 2000px; + opacity: 1; } .meal-sub-item { padding-left: 36px; @@ -839,7 +843,8 @@ body { } .eetmoment-items { - padding: 4px 0; + --collapsible-pt: 4px; + --collapsible-pb: 4px; } .eetmoment-item { @@ -1282,11 +1287,23 @@ body { font-size: var(--font-sm); font-weight: 600; cursor: pointer; - transition: background 0.15s, opacity 0.15s; + transition: background 0.15s, opacity 0.3s ease, max-height 0.3s ease, padding 0.3s ease, margin 0.3s ease, border-width 0.3s ease; text-align: center; font-family: var(--body-font); min-height: var(--touch-min); line-height: 1.3; + max-height: 80px; + overflow: hidden; +} +.save-meal-footer-btn.footer-hidden { + opacity: 0; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-width: 0; + pointer-events: none; } .save-meal-footer-btn:hover { background: var(--primary); @@ -1339,11 +1356,13 @@ body { font-size: var(--font-sm); font-weight: 600; cursor: pointer; - transition: background 0.15s, opacity 0.15s; + transition: background 0.15s, opacity 0.3s ease, max-height 0.3s ease, padding 0.3s ease, margin 0.3s ease, border-width 0.3s ease; text-align: center; font-family: var(--body-font); min-height: var(--touch-min); line-height: 1.3; + max-height: 80px; + overflow: hidden; } .delete-selected-footer-btn:hover { background: var(--danger); @@ -1353,8 +1372,17 @@ body { background: #B71C1C; color: var(--white); } -.delete-selected-footer-btn.hidden { - display: none; +.delete-selected-footer-btn.hidden, +.delete-selected-footer-btn.footer-hidden { + opacity: 0; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-width: 0; + pointer-events: none; + overflow: hidden; } /* ===== Diary — nested meal groups ===== */ @@ -1391,7 +1419,8 @@ body { } .eetmoment-meal-items-wrap { border-top: 1px solid rgba(232, 183, 48, 0.2); - padding: 2px 0; + --collapsible-pt: 2px; + --collapsible-pb: 2px; } .eetmoment-meal-item { display: flex; @@ -2185,6 +2214,11 @@ body { @keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } } @keyframes scaleIn { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.12); } } +@keyframes pulseCount { 0%,100% { transform: scale(1); } 50% { transform: scale(1.3); color: var(--primary); } } +@keyframes fadeInDown { from { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; } to { opacity: 1; max-height: 800px; } } +@keyframes fadeOutUp { from { opacity: 1; max-height: 800px; } to { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; } } +@keyframes checkPop { 0% { transform: scale(1); } 50% { transform: scale(1.3); } 100% { transform: scale(1); } } +@keyframes saveFlash { 0% { box-shadow: 0 0 0 0 rgba(56,161,105,0.4); } 70% { box-shadow: 0 0 0 12px rgba(56,161,105,0); } 100% { box-shadow: 0 0 0 0 rgba(56,161,105,0); } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } /* -- Button press -- */ @@ -2284,6 +2318,58 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; } + +/* ===== Collapsible sections — max-height transition ===== */ +.collapsible { + overflow: hidden; + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, padding 0.3s ease, margin 0.3s ease; + max-height: 2000px; + opacity: 1; + padding-top: var(--collapsible-pt, 4px); + padding-bottom: var(--collapsible-pb, 4px); + margin-top: var(--collapsible-mt, 0); + margin-bottom: var(--collapsible-mb, 0); +} +.collapsible.collapsed { + max-height: 0 !important; + opacity: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + overflow: hidden !important; +} + +/* ===== Item delete fade-out ===== */ +.removing { + animation: fadeOutUp 0.3s ease forwards; + overflow: hidden; + pointer-events: none; +} + +/* ===== Footer buttons: slide in/out ===== */ +.footer-btn-visible { + animation: fadeInDown 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +.footer-btn-hidden { + animation: fadeOutUp 0.25s ease forwards; + pointer-events: none; +} + +/* ===== Checkbox check animation ===== */ +.item-checkbox-wrapper.just-checked { + animation: checkPop 0.25s ease; +} + +/* ===== Glucose save flash ===== */ +.glucose-inline.save-flash { + animation: saveFlash 0.6s ease; +} + +/* ===== Stepper count bump ===== */ +.portie-stepper-count.count-bump { + animation: pulseCount 0.3s ease; +} @@ -2940,6 +3026,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Validate if (val !== null && (isNaN(val) || val < 0 || val > 30)) { + hapticError(); input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : ''; return; } @@ -2974,6 +3061,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); if (el) { el.outerHTML = renderInlineGlucoseItem(m); + // Flash feedback on the new element + requestAnimationFrame(() => { + const newEl = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); + if (newEl) { + newEl.classList.add('save-flash'); + setTimeout(() => newEl.classList.remove('save-flash'), 600); + } + }); } } @@ -3406,7 +3501,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${formatKh(miKh)} g - + `; }).join(''); @@ -3422,15 +3517,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${formatKh(mealKh)} g -
${mealItemsHtml}
-
+
${mealItemsHtml}
+
-
@@ -3460,7 +3555,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
${formatKh(itemKh)} g - + `; }).join(''); @@ -3482,17 +3577,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${items.length > 0 ? '' : ''} - ${items.length > 0 ? `` : ''} + ${items.length > 0 ? `` : ''} ${inlineGlucoseHtml}
+ Voedingsmiddel toevoegen
${items.length > 1 ? ` ` : ''} @@ -3502,7 +3597,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Add event listeners — remove from diary listEl.querySelectorAll('.eetmoment-remove').forEach(btn => { - btn.addEventListener('click', (e) => { + btn.addEventListener('click', async (e) => { e.stopPropagation(); const momentId = btn.dataset.moment; const entry = parseInt(btn.dataset.index); @@ -3510,9 +3605,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const mi = btn.dataset.mi; if (mi !== undefined) { // Removing item from within a meal group - removeMealItemFromDiary(activeDate, momentId, parseInt(groupIdx), parseInt(mi)); + const itemEl = btn.closest('.eetmoment-meal-item'); + await removeMealItemFromDiary(activeDate, momentId, parseInt(groupIdx), parseInt(mi), itemEl); } else { - removeFromMeal(activeDate, momentId, entry); + const itemEl = btn.closest('.eetmoment-item'); + await removeFromMeal(activeDate, momentId, entry, itemEl); } }); }); @@ -3543,10 +3640,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const card = header.closest('.eetmoment-card'); const itemsList = card.querySelector('.eetmoment-items'); if (itemsList) { - itemsList.classList.toggle('hidden'); + itemsList.classList.toggle('collapsed'); const arrow = header.querySelector('span:last-child'); if (arrow) { - arrow.innerHTML = itemsList.classList.contains('hidden') ? '' : ''; + arrow.innerHTML = itemsList.classList.contains('collapsed') ? '' : ''; } } }); @@ -3568,6 +3665,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Checkbox change → update save/delete button state listEl.querySelectorAll('.item-checkbox').forEach(cb => { cb.addEventListener('change', () => { + // Check animation on wrapper + const wrapper = cb.closest('.item-checkbox-wrapper'); + if (wrapper && cb.checked) { + wrapper.classList.add('just-checked'); + setTimeout(() => wrapper.classList.remove('just-checked'), 250); + } const card = cb.closest('.eetmoment-card'); if (!card) return; const footer = card.querySelector('.save-meal-footer'); @@ -3577,13 +3680,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const checked = card.querySelectorAll('.item-checkbox:checked').length; const total = card.querySelectorAll('.item-checkbox').length; if (checked < 2) { - saveBtn.style.display = 'none'; - if (delBtn) delBtn.classList.add('hidden'); + saveBtn.classList.add('footer-hidden'); + if (delBtn) delBtn.classList.add('footer-hidden'); } else { - saveBtn.style.display = ''; + saveBtn.classList.remove('footer-hidden'); saveBtn.disabled = false; - saveBtn.innerHTML = ` Opslaan ${checked} items als maaltijd`; - if (delBtn) delBtn.classList.remove('hidden'); + saveBtn.innerHTML = ` Opslaan ${checked} items als maaltijd`; + if (delBtn) delBtn.classList.remove('footer-hidden'); } }); }); @@ -3783,6 +3886,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Bounce feedback this.classList.add('tapped'); setTimeout(() => this.classList.remove('tapped'), 300); + // Count bump animation + countEl.classList.add('count-bump'); + setTimeout(() => countEl.classList.remove('count-bump'), 300); }); stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { @@ -3792,6 +3898,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Bounce feedback this.classList.add('tapped'); setTimeout(() => this.classList.remove('tapped'), 300); + // Count bump animation + countEl.classList.add('count-bump'); + setTimeout(() => countEl.classList.remove('count-bump'), 300); }); }); @@ -3852,7 +3961,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); } - async function removeFromMeal(dateStr, momentId, index) { + async function removeFromMeal(dateStr, momentId, index, itemEl) { const day = dagboek[dateStr]; if (day && day[momentId]) { const target = day[momentId][index]; @@ -3862,6 +3971,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- message: `Weet je zeker dat je "${name}" wilt verwijderen?` }); if (!confirmed) return; + // Animate fade-out before removing data + if (itemEl) { + itemEl.classList.add('removing'); + await new Promise(r => setTimeout(r, 280)); + } day[momentId].splice(index, 1); saveToStorage(); renderDagboek(); @@ -3873,7 +3987,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- * Remove a single item from within a meal group in the diary. * If the last item is removed, removes the entire meal group. */ - async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { + async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi, itemEl) { const day = dagboek[dateStr]; if (!day || !day[momentId]) return; const entry = day[momentId][entryIdx]; @@ -3884,6 +3998,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?` }); if (!confirmed) return; + // Animate fade-out before removing data + if (itemEl) { + itemEl.classList.add('removing'); + await new Promise(r => setTimeout(r, 280)); + } entry.items.splice(mi, 1); // If last item — remove the whole meal group entry if (entry.items.length === 0) { @@ -3963,6 +4082,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } overlay.querySelector('#portieEditClose').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); + setupSwipeDown(overlay, closeDialog); const input = overlay.querySelector('#portieEditInput'); const preview = overlay.querySelector('#portieEditPreview'); @@ -4151,6 +4271,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#bulkEditClose').addEventListener('click', closeDialog); overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); + setupSwipeDown(overlay, closeDialog); function collectPortions() { const portions = []; @@ -4519,13 +4640,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const input = overlay.querySelector('#mealNameInput'); function close() { - overlay.remove(); - document.body.style.overflow = ''; + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); } function save() { const name = input.value.trim(); if (!name) { + hapticError(); input.style.borderColor = 'var(--danger)'; input.focus(); return; @@ -4548,6 +4670,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); overlay.querySelector('#mealNameCancel').addEventListener('click', close); overlay.querySelector('#mealNameSave').addEventListener('click', save); + setupSwipeDown(overlay, close); // Enter key saves, Escape closes input.addEventListener('keydown', (e) => { @@ -4584,7 +4707,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
${escapeHtml(meal.naam)}
${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh
- +
Toevoegen aan dagboek @@ -4675,16 +4798,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.style.overflow = 'hidden'; // Close handlers - overlay.querySelector('.modal-close').addEventListener('click', () => { - overlay.remove(); - document.body.style.overflow = ''; - }); + function closeAddToMeal() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } + overlay.querySelector('.modal-close').addEventListener('click', closeAddToMeal); overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); - document.body.style.overflow = ''; - } + if (e.target === overlay) closeAddToMeal(); }); + setupSwipeDown(overlay, closeAddToMeal); // Date picker navigation (event delegation on overlay) overlay.addEventListener('click', (e) => { @@ -4798,6 +4920,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (portie > 0) newItems.push({ item: e.item, portie: portie }); }); if (newItems.length === 0) { + hapticError(); showToast('Een maaltijd moet minimaal 1 item hebben'); return; } @@ -4820,6 +4943,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#mealDiaryClose').addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => { btn.addEventListener('click', () => { @@ -4844,6 +4968,91 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, 250); } + /** + * Add swipe-down-to-close gesture to a bottom sheet overlay. + * @param {HTMLElement} overlay - The .modal-overlay element containing a .modal-sheet + * @param {Function} closeCallback - Called when swipe-down completes + */ + function setupSwipeDown(overlay, closeCallback) { + const sheet = overlay.querySelector('.modal-sheet'); + if (!sheet) return; + + let startY = 0; + let currentY = 0; + let isDragging = false; + let startTime = 0; + let hasSwiped = false; + + function resetInlineStyles() { + sheet.style.transform = ''; + sheet.style.transition = ''; + overlay.style.opacity = ''; + overlay.style.background = ''; + overlay.style.transition = ''; + } + + sheet.addEventListener('touchstart', (e) => { + if (e.touches.length !== 1) return; + // Only initiate swipe-down when scrolled to top (native feel) + if (sheet.scrollTop > 0) return; + const touch = e.touches[0]; + startY = touch.clientY; + currentY = startY; + isDragging = true; + hasSwiped = false; + startTime = Date.now(); + sheet.style.transition = 'none'; + overlay.style.transition = 'none'; + }, { passive: true }); + + sheet.addEventListener('touchmove', (e) => { + if (!isDragging) return; + const touch = e.touches[0]; + currentY = touch.clientY; + const deltaY = currentY - startY; + if (deltaY > 0) { + hasSwiped = true; + e.preventDefault(); + // Follow finger, capped at 80% of viewport + const translateY = Math.min(deltaY, window.innerHeight * 0.8); + sheet.style.transform = `translateY(${translateY}px)`; + // Dim overlay as the sheet goes down + overlay.style.background = `rgba(0,0,0,${Math.max(0, 0.4 * (1 - translateY / window.innerHeight))})`; + } + }, { passive: false }); + + sheet.addEventListener('touchend', () => { + if (!isDragging) return; + isDragging = false; + if (!hasSwiped) return; + + const deltaY = currentY - startY; + const elapsed = Date.now() - startTime || 1; + const velocity = deltaY / elapsed; // px/ms + + if (deltaY > 80 || velocity > 0.5) { + // Swiped far enough — animate the rest of the way down and close + sheet.style.transition = 'transform 0.25s ease-in'; + overlay.style.transition = 'opacity 0.2s ease, background 0.25s ease'; + sheet.style.transform = `translateY(${sheet.offsetHeight}px)`; + overlay.style.opacity = '0'; + setTimeout(() => { + resetInlineStyles(); + closeCallback(); + }, 260); + } else { + // Not enough swipe — snap back + sheet.style.transition = 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; + overlay.style.transition = 'background 0.3s ease'; + sheet.style.transform = ''; + overlay.style.background = ''; + setTimeout(() => { + overlay.style.transition = ''; + }, 350); + } + }, { passive: true }); + } + function showToast(message) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); @@ -4886,7 +5095,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
- +
`; @@ -4900,6 +5109,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#confirmNo').addEventListener('click', () => { close(); resolve(false); }); overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } }); + setupSwipeDown(overlay, () => { close(); resolve(false); }); }); } @@ -4938,6 +5148,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); } function showNameInputModal(title, defaultValue, onSave) { @@ -4972,6 +5183,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#nameInputCancel').addEventListener('click', close); overlay.querySelector('#nameInputSave').addEventListener('click', submit); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); }); setTimeout(() => { input.focus(); input.select(); }, 100); } @@ -4994,7 +5206,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- g - + `; @@ -5018,7 +5230,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
- +
@@ -5045,6 +5257,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } }); if (newItems.length === 0) { + hapticError(); showToast('Een maaltijd moet minimaal 1 item hebben'); return; } @@ -5071,6 +5284,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#mealEditClose').addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => { btn.addEventListener('click', () => { @@ -5104,9 +5318,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- `; document.body.appendChild(searchOverlay); - function closeSearch() { searchOverlay.remove(); } + function closeSearch() { + searchOverlay.classList.add('closing'); + setTimeout(() => searchOverlay.remove(), 280); + } searchOverlay.querySelector('.modal-close').addEventListener('click', closeSearch); searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); }); + setupSwipeDown(searchOverlay, closeSearch); const searchInput = searchOverlay.querySelector('#mealItemSearch'); const resultsEl = searchOverlay.querySelector('#mealSearchResults'); searchInput.addEventListener('input', () => { @@ -5187,6 +5405,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- po.querySelector('#mealPortionClose').addEventListener('click', closePicker); po.querySelector('#mealPortionCancel').addEventListener('click', closePicker); po.addEventListener('click', (e) => { if (e.target === po) closePicker(); }); + setupSwipeDown(po, closePicker); const input = po.querySelector('#mealPortionInput'); const preview = po.querySelector('#mealPortionPreview'); input.addEventListener('input', () => { @@ -5227,6 +5446,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- po.querySelector('#mealPortionConfirm').addEventListener('click', () => { const portie = parseFloat(input.value) || 0; if (portie <= 0) { + hapticError(); input.focus(); input.style.borderColor = 'var(--danger)'; setTimeout(() => { input.style.borderColor = ''; }, 1500); return; @@ -5402,6 +5622,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('addMealModal')) closeModal('addMealModal'); }); + // Swipe-down to close for static modals + setupSwipeDown(document.getElementById('detailModal'), () => closeModal('detailModal')); + setupSwipeDown(document.getElementById('addMealModal'), () => closeModal('addMealModal')); + // Settings button & modal document.getElementById('settingsBtn').addEventListener('click', () => { // Populate with current limit @@ -5416,6 +5640,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('settingsModal')) closeModal('settingsModal'); }); + setupSwipeDown(document.getElementById('settingsModal'), () => closeModal('settingsModal')); + document.getElementById('saveCarbLimitBtn').addEventListener('click', () => { const input = document.getElementById('carbLimitInput'); const val = parseInt(input.value, 10); @@ -5484,11 +5710,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const wrap = group.querySelector('.eetmoment-meal-items-wrap'); const actions = group.querySelector('.eetmoment-meal-actions'); if (wrap) { - wrap.classList.toggle('hidden'); - if (actions) actions.classList.toggle('hidden'); + wrap.classList.toggle('collapsed'); + if (actions) actions.classList.toggle('collapsed'); const arrow = btn.querySelector('span:last-child i'); if (arrow) { - arrow.className = wrap.classList.contains('hidden') ? 'fas fa-chevron-down' : 'fas fa-chevron-up'; + arrow.className = wrap.classList.contains('collapsed') ? 'fas fa-chevron-down' : 'fas fa-chevron-up'; } } } @@ -5549,6 +5775,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); }); overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); }); + setupSwipeDown(overlay, close); } }); } @@ -5556,17 +5783,29 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Start the app // Track last click position for toast positioning + haptic feedback + // Supports data-haptic="strong" (18ms) for destructive actions (function setupHaptics() { document.addEventListener('click', (e) => { lastClickY = e.clientY; if (typeof navigator !== 'undefined' && navigator.vibrate) { - if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { - navigator.vibrate(8); + const btn = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button'); + if (btn) { + const strength = btn.dataset.haptic || (btn.closest('[data-haptic]') && btn.closest('[data-haptic]').dataset.haptic) || 'normal'; + navigator.vibrate(strength === 'strong' ? 18 : 8); } } }, { passive: true }); })(); + /** + * Short double-buzz on validation errors (empty name, invalid portion, etc.) + */ + function hapticError() { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate([30, 50, 30]); + } + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { diff --git a/index.html b/index.html index bf0c76c..07c8eed 100644 --- a/index.html +++ b/index.html @@ -739,11 +739,15 @@ body { background: var(--primary-light); } .meal-group-items { - display: none; + max-height: 0; + opacity: 0; + overflow: hidden; border-top: 1px solid var(--border); + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; } .meal-group-header.expanded + .meal-group-items { - display: block; + max-height: 2000px; + opacity: 1; } .meal-sub-item { padding-left: 36px; @@ -839,7 +843,8 @@ body { } .eetmoment-items { - padding: 4px 0; + --collapsible-pt: 4px; + --collapsible-pb: 4px; } .eetmoment-item { @@ -1282,11 +1287,23 @@ body { font-size: var(--font-sm); font-weight: 600; cursor: pointer; - transition: background 0.15s, opacity 0.15s; + transition: background 0.15s, opacity 0.3s ease, max-height 0.3s ease, padding 0.3s ease, margin 0.3s ease, border-width 0.3s ease; text-align: center; font-family: var(--body-font); min-height: var(--touch-min); line-height: 1.3; + max-height: 80px; + overflow: hidden; +} +.save-meal-footer-btn.footer-hidden { + opacity: 0; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-width: 0; + pointer-events: none; } .save-meal-footer-btn:hover { background: var(--primary); @@ -1339,11 +1356,13 @@ body { font-size: var(--font-sm); font-weight: 600; cursor: pointer; - transition: background 0.15s, opacity 0.15s; + transition: background 0.15s, opacity 0.3s ease, max-height 0.3s ease, padding 0.3s ease, margin 0.3s ease, border-width 0.3s ease; text-align: center; font-family: var(--body-font); min-height: var(--touch-min); line-height: 1.3; + max-height: 80px; + overflow: hidden; } .delete-selected-footer-btn:hover { background: var(--danger); @@ -1353,8 +1372,17 @@ body { background: #B71C1C; color: var(--white); } -.delete-selected-footer-btn.hidden { - display: none; +.delete-selected-footer-btn.hidden, +.delete-selected-footer-btn.footer-hidden { + opacity: 0; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-width: 0; + pointer-events: none; + overflow: hidden; } /* ===== Diary — nested meal groups ===== */ @@ -1391,7 +1419,8 @@ body { } .eetmoment-meal-items-wrap { border-top: 1px solid rgba(232, 183, 48, 0.2); - padding: 2px 0; + --collapsible-pt: 2px; + --collapsible-pb: 2px; } .eetmoment-meal-item { display: flex; @@ -2185,6 +2214,11 @@ body { @keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } } @keyframes scaleIn { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.12); } } +@keyframes pulseCount { 0%,100% { transform: scale(1); } 50% { transform: scale(1.3); color: var(--primary); } } +@keyframes fadeInDown { from { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; } to { opacity: 1; max-height: 800px; } } +@keyframes fadeOutUp { from { opacity: 1; max-height: 800px; } to { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; } } +@keyframes checkPop { 0% { transform: scale(1); } 50% { transform: scale(1.3); } 100% { transform: scale(1); } } +@keyframes saveFlash { 0% { box-shadow: 0 0 0 0 rgba(56,161,105,0.4); } 70% { box-shadow: 0 0 0 12px rgba(56,161,105,0); } 100% { box-shadow: 0 0 0 0 rgba(56,161,105,0); } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } /* -- Button press -- */ @@ -2284,6 +2318,58 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; } + +/* ===== Collapsible sections — max-height transition ===== */ +.collapsible { + overflow: hidden; + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, padding 0.3s ease, margin 0.3s ease; + max-height: 2000px; + opacity: 1; + padding-top: var(--collapsible-pt, 4px); + padding-bottom: var(--collapsible-pb, 4px); + margin-top: var(--collapsible-mt, 0); + margin-bottom: var(--collapsible-mb, 0); +} +.collapsible.collapsed { + max-height: 0 !important; + opacity: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + overflow: hidden !important; +} + +/* ===== Item delete fade-out ===== */ +.removing { + animation: fadeOutUp 0.3s ease forwards; + overflow: hidden; + pointer-events: none; +} + +/* ===== Footer buttons: slide in/out ===== */ +.footer-btn-visible { + animation: fadeInDown 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} +.footer-btn-hidden { + animation: fadeOutUp 0.25s ease forwards; + pointer-events: none; +} + +/* ===== Checkbox check animation ===== */ +.item-checkbox-wrapper.just-checked { + animation: checkPop 0.25s ease; +} + +/* ===== Glucose save flash ===== */ +.glucose-inline.save-flash { + animation: saveFlash 0.6s ease; +} + +/* ===== Stepper count bump ===== */ +.portie-stepper-count.count-bump { + animation: pulseCount 0.3s ease; +} @@ -2940,6 +3026,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Validate if (val !== null && (isNaN(val) || val < 0 || val > 30)) { + hapticError(); input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : ''; return; } @@ -2974,6 +3061,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); if (el) { el.outerHTML = renderInlineGlucoseItem(m); + // Flash feedback on the new element + requestAnimationFrame(() => { + const newEl = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); + if (newEl) { + newEl.classList.add('save-flash'); + setTimeout(() => newEl.classList.remove('save-flash'), 600); + } + }); } } @@ -3406,7 +3501,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${formatKh(miKh)} g - + `; }).join(''); @@ -3422,15 +3517,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${formatKh(mealKh)} g -
${mealItemsHtml}
-
+
${mealItemsHtml}
+
-
@@ -3460,7 +3555,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
${formatKh(itemKh)} g - + `; }).join(''); @@ -3482,17 +3577,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${items.length > 0 ? '' : ''} - ${items.length > 0 ? `` : ''} + ${items.length > 0 ? `` : ''} ${inlineGlucoseHtml}
+ Voedingsmiddel toevoegen
${items.length > 1 ? ` ` : ''} @@ -3502,7 +3597,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Add event listeners — remove from diary listEl.querySelectorAll('.eetmoment-remove').forEach(btn => { - btn.addEventListener('click', (e) => { + btn.addEventListener('click', async (e) => { e.stopPropagation(); const momentId = btn.dataset.moment; const entry = parseInt(btn.dataset.index); @@ -3510,9 +3605,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const mi = btn.dataset.mi; if (mi !== undefined) { // Removing item from within a meal group - removeMealItemFromDiary(activeDate, momentId, parseInt(groupIdx), parseInt(mi)); + const itemEl = btn.closest('.eetmoment-meal-item'); + await removeMealItemFromDiary(activeDate, momentId, parseInt(groupIdx), parseInt(mi), itemEl); } else { - removeFromMeal(activeDate, momentId, entry); + const itemEl = btn.closest('.eetmoment-item'); + await removeFromMeal(activeDate, momentId, entry, itemEl); } }); }); @@ -3543,10 +3640,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const card = header.closest('.eetmoment-card'); const itemsList = card.querySelector('.eetmoment-items'); if (itemsList) { - itemsList.classList.toggle('hidden'); + itemsList.classList.toggle('collapsed'); const arrow = header.querySelector('span:last-child'); if (arrow) { - arrow.innerHTML = itemsList.classList.contains('hidden') ? '' : ''; + arrow.innerHTML = itemsList.classList.contains('collapsed') ? '' : ''; } } }); @@ -3568,6 +3665,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Checkbox change → update save/delete button state listEl.querySelectorAll('.item-checkbox').forEach(cb => { cb.addEventListener('change', () => { + // Check animation on wrapper + const wrapper = cb.closest('.item-checkbox-wrapper'); + if (wrapper && cb.checked) { + wrapper.classList.add('just-checked'); + setTimeout(() => wrapper.classList.remove('just-checked'), 250); + } const card = cb.closest('.eetmoment-card'); if (!card) return; const footer = card.querySelector('.save-meal-footer'); @@ -3577,13 +3680,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const checked = card.querySelectorAll('.item-checkbox:checked').length; const total = card.querySelectorAll('.item-checkbox').length; if (checked < 2) { - saveBtn.style.display = 'none'; - if (delBtn) delBtn.classList.add('hidden'); + saveBtn.classList.add('footer-hidden'); + if (delBtn) delBtn.classList.add('footer-hidden'); } else { - saveBtn.style.display = ''; + saveBtn.classList.remove('footer-hidden'); saveBtn.disabled = false; - saveBtn.innerHTML = ` Opslaan ${checked} items als maaltijd`; - if (delBtn) delBtn.classList.remove('hidden'); + saveBtn.innerHTML = ` Opslaan ${checked} items als maaltijd`; + if (delBtn) delBtn.classList.remove('footer-hidden'); } }); }); @@ -3783,6 +3886,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Bounce feedback this.classList.add('tapped'); setTimeout(() => this.classList.remove('tapped'), 300); + // Count bump animation + countEl.classList.add('count-bump'); + setTimeout(() => countEl.classList.remove('count-bump'), 300); }); stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { @@ -3792,6 +3898,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Bounce feedback this.classList.add('tapped'); setTimeout(() => this.classList.remove('tapped'), 300); + // Count bump animation + countEl.classList.add('count-bump'); + setTimeout(() => countEl.classList.remove('count-bump'), 300); }); }); @@ -3852,7 +3961,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); } - async function removeFromMeal(dateStr, momentId, index) { + async function removeFromMeal(dateStr, momentId, index, itemEl) { const day = dagboek[dateStr]; if (day && day[momentId]) { const target = day[momentId][index]; @@ -3862,6 +3971,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- message: `Weet je zeker dat je "${name}" wilt verwijderen?` }); if (!confirmed) return; + // Animate fade-out before removing data + if (itemEl) { + itemEl.classList.add('removing'); + await new Promise(r => setTimeout(r, 280)); + } day[momentId].splice(index, 1); saveToStorage(); renderDagboek(); @@ -3873,7 +3987,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- * Remove a single item from within a meal group in the diary. * If the last item is removed, removes the entire meal group. */ - async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { + async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi, itemEl) { const day = dagboek[dateStr]; if (!day || !day[momentId]) return; const entry = day[momentId][entryIdx]; @@ -3884,6 +3998,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?` }); if (!confirmed) return; + // Animate fade-out before removing data + if (itemEl) { + itemEl.classList.add('removing'); + await new Promise(r => setTimeout(r, 280)); + } entry.items.splice(mi, 1); // If last item — remove the whole meal group entry if (entry.items.length === 0) { @@ -3963,6 +4082,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } overlay.querySelector('#portieEditClose').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); + setupSwipeDown(overlay, closeDialog); const input = overlay.querySelector('#portieEditInput'); const preview = overlay.querySelector('#portieEditPreview'); @@ -4151,6 +4271,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#bulkEditClose').addEventListener('click', closeDialog); overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); + setupSwipeDown(overlay, closeDialog); function collectPortions() { const portions = []; @@ -4519,13 +4640,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const input = overlay.querySelector('#mealNameInput'); function close() { - overlay.remove(); - document.body.style.overflow = ''; + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); } function save() { const name = input.value.trim(); if (!name) { + hapticError(); input.style.borderColor = 'var(--danger)'; input.focus(); return; @@ -4548,6 +4670,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); overlay.querySelector('#mealNameCancel').addEventListener('click', close); overlay.querySelector('#mealNameSave').addEventListener('click', save); + setupSwipeDown(overlay, close); // Enter key saves, Escape closes input.addEventListener('keydown', (e) => { @@ -4584,7 +4707,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
${escapeHtml(meal.naam)}
${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh
- +
Toevoegen aan dagboek @@ -4675,16 +4798,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.style.overflow = 'hidden'; // Close handlers - overlay.querySelector('.modal-close').addEventListener('click', () => { - overlay.remove(); - document.body.style.overflow = ''; - }); + function closeAddToMeal() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } + overlay.querySelector('.modal-close').addEventListener('click', closeAddToMeal); overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); - document.body.style.overflow = ''; - } + if (e.target === overlay) closeAddToMeal(); }); + setupSwipeDown(overlay, closeAddToMeal); // Date picker navigation (event delegation on overlay) overlay.addEventListener('click', (e) => { @@ -4798,6 +4920,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (portie > 0) newItems.push({ item: e.item, portie: portie }); }); if (newItems.length === 0) { + hapticError(); showToast('Een maaltijd moet minimaal 1 item hebben'); return; } @@ -4820,6 +4943,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#mealDiaryClose').addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => { btn.addEventListener('click', () => { @@ -4844,6 +4968,91 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, 250); } + /** + * Add swipe-down-to-close gesture to a bottom sheet overlay. + * @param {HTMLElement} overlay - The .modal-overlay element containing a .modal-sheet + * @param {Function} closeCallback - Called when swipe-down completes + */ + function setupSwipeDown(overlay, closeCallback) { + const sheet = overlay.querySelector('.modal-sheet'); + if (!sheet) return; + + let startY = 0; + let currentY = 0; + let isDragging = false; + let startTime = 0; + let hasSwiped = false; + + function resetInlineStyles() { + sheet.style.transform = ''; + sheet.style.transition = ''; + overlay.style.opacity = ''; + overlay.style.background = ''; + overlay.style.transition = ''; + } + + sheet.addEventListener('touchstart', (e) => { + if (e.touches.length !== 1) return; + // Only initiate swipe-down when scrolled to top (native feel) + if (sheet.scrollTop > 0) return; + const touch = e.touches[0]; + startY = touch.clientY; + currentY = startY; + isDragging = true; + hasSwiped = false; + startTime = Date.now(); + sheet.style.transition = 'none'; + overlay.style.transition = 'none'; + }, { passive: true }); + + sheet.addEventListener('touchmove', (e) => { + if (!isDragging) return; + const touch = e.touches[0]; + currentY = touch.clientY; + const deltaY = currentY - startY; + if (deltaY > 0) { + hasSwiped = true; + e.preventDefault(); + // Follow finger, capped at 80% of viewport + const translateY = Math.min(deltaY, window.innerHeight * 0.8); + sheet.style.transform = `translateY(${translateY}px)`; + // Dim overlay as the sheet goes down + overlay.style.background = `rgba(0,0,0,${Math.max(0, 0.4 * (1 - translateY / window.innerHeight))})`; + } + }, { passive: false }); + + sheet.addEventListener('touchend', () => { + if (!isDragging) return; + isDragging = false; + if (!hasSwiped) return; + + const deltaY = currentY - startY; + const elapsed = Date.now() - startTime || 1; + const velocity = deltaY / elapsed; // px/ms + + if (deltaY > 80 || velocity > 0.5) { + // Swiped far enough — animate the rest of the way down and close + sheet.style.transition = 'transform 0.25s ease-in'; + overlay.style.transition = 'opacity 0.2s ease, background 0.25s ease'; + sheet.style.transform = `translateY(${sheet.offsetHeight}px)`; + overlay.style.opacity = '0'; + setTimeout(() => { + resetInlineStyles(); + closeCallback(); + }, 260); + } else { + // Not enough swipe — snap back + sheet.style.transition = 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; + overlay.style.transition = 'background 0.3s ease'; + sheet.style.transform = ''; + overlay.style.background = ''; + setTimeout(() => { + overlay.style.transition = ''; + }, 350); + } + }, { passive: true }); + } + function showToast(message) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); @@ -4886,7 +5095,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
- +
`; @@ -4900,6 +5109,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#confirmNo').addEventListener('click', () => { close(); resolve(false); }); overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } }); + setupSwipeDown(overlay, () => { close(); resolve(false); }); }); } @@ -4938,6 +5148,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); } function showNameInputModal(title, defaultValue, onSave) { @@ -4972,6 +5183,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#nameInputCancel').addEventListener('click', close); overlay.querySelector('#nameInputSave').addEventListener('click', submit); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); }); setTimeout(() => { input.focus(); input.select(); }, 100); } @@ -4994,7 +5206,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- g - + `; @@ -5018,7 +5230,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
- +
@@ -5045,6 +5257,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } }); if (newItems.length === 0) { + hapticError(); showToast('Een maaltijd moet minimaal 1 item hebben'); return; } @@ -5071,6 +5284,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- overlay.querySelector('#mealEditClose').addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + setupSwipeDown(overlay, close); overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => { btn.addEventListener('click', () => { @@ -5104,9 +5318,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- `; document.body.appendChild(searchOverlay); - function closeSearch() { searchOverlay.remove(); } + function closeSearch() { + searchOverlay.classList.add('closing'); + setTimeout(() => searchOverlay.remove(), 280); + } searchOverlay.querySelector('.modal-close').addEventListener('click', closeSearch); searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); }); + setupSwipeDown(searchOverlay, closeSearch); const searchInput = searchOverlay.querySelector('#mealItemSearch'); const resultsEl = searchOverlay.querySelector('#mealSearchResults'); searchInput.addEventListener('input', () => { @@ -5187,6 +5405,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- po.querySelector('#mealPortionClose').addEventListener('click', closePicker); po.querySelector('#mealPortionCancel').addEventListener('click', closePicker); po.addEventListener('click', (e) => { if (e.target === po) closePicker(); }); + setupSwipeDown(po, closePicker); const input = po.querySelector('#mealPortionInput'); const preview = po.querySelector('#mealPortionPreview'); input.addEventListener('input', () => { @@ -5227,6 +5446,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- po.querySelector('#mealPortionConfirm').addEventListener('click', () => { const portie = parseFloat(input.value) || 0; if (portie <= 0) { + hapticError(); input.focus(); input.style.borderColor = 'var(--danger)'; setTimeout(() => { input.style.borderColor = ''; }, 1500); return; @@ -5402,6 +5622,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('addMealModal')) closeModal('addMealModal'); }); + // Swipe-down to close for static modals + setupSwipeDown(document.getElementById('detailModal'), () => closeModal('detailModal')); + setupSwipeDown(document.getElementById('addMealModal'), () => closeModal('addMealModal')); + // Settings button & modal document.getElementById('settingsBtn').addEventListener('click', () => { // Populate with current limit @@ -5416,6 +5640,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('settingsModal')) closeModal('settingsModal'); }); + setupSwipeDown(document.getElementById('settingsModal'), () => closeModal('settingsModal')); + document.getElementById('saveCarbLimitBtn').addEventListener('click', () => { const input = document.getElementById('carbLimitInput'); const val = parseInt(input.value, 10); @@ -5484,11 +5710,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const wrap = group.querySelector('.eetmoment-meal-items-wrap'); const actions = group.querySelector('.eetmoment-meal-actions'); if (wrap) { - wrap.classList.toggle('hidden'); - if (actions) actions.classList.toggle('hidden'); + wrap.classList.toggle('collapsed'); + if (actions) actions.classList.toggle('collapsed'); const arrow = btn.querySelector('span:last-child i'); if (arrow) { - arrow.className = wrap.classList.contains('hidden') ? 'fas fa-chevron-down' : 'fas fa-chevron-up'; + arrow.className = wrap.classList.contains('collapsed') ? 'fas fa-chevron-down' : 'fas fa-chevron-up'; } } } @@ -5549,6 +5775,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); }); overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); }); + setupSwipeDown(overlay, close); } }); } @@ -5556,17 +5783,29 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Start the app // Track last click position for toast positioning + haptic feedback + // Supports data-haptic="strong" (18ms) for destructive actions (function setupHaptics() { document.addEventListener('click', (e) => { lastClickY = e.clientY; if (typeof navigator !== 'undefined' && navigator.vibrate) { - if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { - navigator.vibrate(8); + const btn = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button'); + if (btn) { + const strength = btn.dataset.haptic || (btn.closest('[data-haptic]') && btn.closest('[data-haptic]').dataset.haptic) || 'normal'; + navigator.vibrate(strength === 'strong' ? 18 : 8); } } }, { passive: true }); })(); + /** + * Short double-buzz on validation errors (empty name, invalid portion, etc.) + */ + function hapticError() { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate([30, 50, 30]); + } + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else {