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-
Nee
- ${escapeHtml(confirmText)}
+ ${escapeHtml(confirmText)}
`;
@@ -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-
Wijzigingen opslaan
- Maaltijd verwijderen
+ Maaltijd verwijderen
@@ -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}
+
Porties
Verplaatsen
-
+
Verwijderen
@@ -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-
Nee
- ${escapeHtml(confirmText)}
+ ${escapeHtml(confirmText)}
`;
@@ -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-
Wijzigingen opslaan
- Maaltijd verwijderen
+ Maaltijd verwijderen
@@ -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 {