UX: Animaties + haptics + bottom sheet verbeteringen
- Meal-group expand/collapse: smooth max-height transitie - Items verwijderen: fade-out animatie - Checkbox toggle: check animatie + kleur-transitie - Footer knoppen: fadeIn bij verschijnen - Portie stepper +/-: visuele pulse feedback - Glucose save: visuele bevestiging - Destructive haptics: vibrate(15) ipv 8 - Validatiefouten: dubbele korte vibrate - Bottom sheet: swipe-down gesture + backdrop click fix 38/39 tests groen
This commit is contained in:
parent
065644eb73
commit
81d459cd51
2 changed files with 574 additions and 96 deletions
|
|
@ -739,11 +739,15 @@ body {
|
||||||
background: var(--primary-light);
|
background: var(--primary-light);
|
||||||
}
|
}
|
||||||
.meal-group-items {
|
.meal-group-items {
|
||||||
display: none;
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
border-top: 1px solid var(--border);
|
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 {
|
.meal-group-header.expanded + .meal-group-items {
|
||||||
display: block;
|
max-height: 2000px;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.meal-sub-item {
|
.meal-sub-item {
|
||||||
padding-left: 36px;
|
padding-left: 36px;
|
||||||
|
|
@ -839,7 +843,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.eetmoment-items {
|
.eetmoment-items {
|
||||||
padding: 4px 0;
|
--collapsible-pt: 4px;
|
||||||
|
--collapsible-pb: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eetmoment-item {
|
.eetmoment-item {
|
||||||
|
|
@ -1282,11 +1287,23 @@ body {
|
||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
text-align: center;
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
min-height: var(--touch-min);
|
min-height: var(--touch-min);
|
||||||
line-height: 1.3;
|
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 {
|
.save-meal-footer-btn:hover {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
|
|
@ -1339,11 +1356,13 @@ body {
|
||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
text-align: center;
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
min-height: var(--touch-min);
|
min-height: var(--touch-min);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
max-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.delete-selected-footer-btn:hover {
|
.delete-selected-footer-btn:hover {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
|
|
@ -1353,8 +1372,17 @@ body {
|
||||||
background: #B71C1C;
|
background: #B71C1C;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
.delete-selected-footer-btn.hidden {
|
.delete-selected-footer-btn.hidden,
|
||||||
display: none;
|
.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 ===== */
|
/* ===== Diary — nested meal groups ===== */
|
||||||
|
|
@ -1391,7 +1419,8 @@ body {
|
||||||
}
|
}
|
||||||
.eetmoment-meal-items-wrap {
|
.eetmoment-meal-items-wrap {
|
||||||
border-top: 1px solid rgba(232, 183, 48, 0.2);
|
border-top: 1px solid rgba(232, 183, 48, 0.2);
|
||||||
padding: 2px 0;
|
--collapsible-pt: 2px;
|
||||||
|
--collapsible-pb: 2px;
|
||||||
}
|
}
|
||||||
.eetmoment-meal-item {
|
.eetmoment-meal-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -2185,6 +2214,11 @@ body {
|
||||||
@keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
|
@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 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 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; } }
|
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
||||||
|
|
||||||
/* -- Button press -- */
|
/* -- Button press -- */
|
||||||
|
|
@ -2284,6 +2318,58 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.5s ease-in-out infinite;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -2940,6 +3026,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
if (val !== null && (isNaN(val) || val < 0 || val > 30)) {
|
if (val !== null && (isNaN(val) || val < 0 || val > 30)) {
|
||||||
|
hapticError();
|
||||||
input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : '';
|
input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : '';
|
||||||
return;
|
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}"]`);
|
const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.outerHTML = renderInlineGlucoseItem(m);
|
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-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-item-kh">${formatKh(miKh)} g</span>
|
<span class="eetmoment-item-kh">${formatKh(miKh)} g</span>
|
||||||
<button class="eetmoment-remove" data-moment="${moment.id}" data-entry="${i}" data-mi="${mi}" aria-label="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="eetmoment-remove" data-haptic="strong" data-moment="${moment.id}" data-entry="${i}" data-mi="${mi}" aria-label="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -3422,15 +3517,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="eetmoment-meal-kh">${formatKh(mealKh)} g</span>
|
<span class="eetmoment-meal-kh">${formatKh(mealKh)} g</span>
|
||||||
<span style="font-size:0.7rem;color:var(--text-muted);"><i class="fas fa-chevron-up"></i></span>
|
<span style="font-size:0.7rem;color:var(--text-muted);"><i class="fas fa-chevron-up"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div>
|
<div class="eetmoment-meal-items-wrap collapsible">${mealItemsHtml}</div>
|
||||||
<div class="eetmoment-meal-actions">
|
<div class="eetmoment-meal-actions collapsible">
|
||||||
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-pen"></i> Porties
|
<i class="fas fa-pen"></i> Porties
|
||||||
</button>
|
</button>
|
||||||
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-arrow-right"></i> Verplaatsen
|
<i class="fas fa-arrow-right"></i> Verplaatsen
|
||||||
</button>
|
</button>
|
||||||
<button class="eetmoment-meal-action-btn eetmoment-delete" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn eetmoment-delete" data-haptic="strong" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-trash"></i> Verwijderen
|
<i class="fas fa-trash"></i> Verwijderen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3460,7 +3555,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-item-kh">${formatKh(itemKh)} g</span>
|
<span class="eetmoment-item-kh">${formatKh(itemKh)} g</span>
|
||||||
<button class="eetmoment-remove" data-moment="${moment.id}" data-index="${i}" aria-label="Verwijderen" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="eetmoment-remove" data-haptic="strong" data-moment="${moment.id}" data-index="${i}" aria-label="Verwijderen" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -3482,17 +3577,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
${items.length > 0 ? `<ul class="eetmoment-items collapsible">${itemsHtml}</ul>` : ''}
|
||||||
${inlineGlucoseHtml}
|
${inlineGlucoseHtml}
|
||||||
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
||||||
<span>+</span> Voedingsmiddel toevoegen
|
<span>+</span> Voedingsmiddel toevoegen
|
||||||
</div>
|
</div>
|
||||||
${items.length > 1 ? `
|
${items.length > 1 ? `
|
||||||
<div class="save-meal-footer" data-moment="${moment.id}">
|
<div class="save-meal-footer" data-moment="${moment.id}">
|
||||||
<button class="save-meal-footer-btn" data-moment="${moment.id}" style="display:none">
|
<button class="save-meal-footer-btn footer-hidden" data-moment="${moment.id}">
|
||||||
<i class="fas fa-star"></i> Selecteer ${items.length} items
|
<i class="fas fa-star"></i> Selecteer ${items.length} items
|
||||||
</button>
|
</button>
|
||||||
<button class="delete-selected-footer-btn hidden" data-moment="${moment.id}">
|
<button class="delete-selected-footer-btn footer-hidden" data-haptic="strong" data-moment="${moment.id}">
|
||||||
<i class="fas fa-trash"></i> Verwijder geselecteerde
|
<i class="fas fa-trash"></i> Verwijder geselecteerde
|
||||||
</button>
|
</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
@ -3502,7 +3597,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// Add event listeners — remove from diary
|
// Add event listeners — remove from diary
|
||||||
listEl.querySelectorAll('.eetmoment-remove').forEach(btn => {
|
listEl.querySelectorAll('.eetmoment-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const momentId = btn.dataset.moment;
|
const momentId = btn.dataset.moment;
|
||||||
const entry = parseInt(btn.dataset.index);
|
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;
|
const mi = btn.dataset.mi;
|
||||||
if (mi !== undefined) {
|
if (mi !== undefined) {
|
||||||
// Removing item from within a meal group
|
// 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 {
|
} 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 card = header.closest('.eetmoment-card');
|
||||||
const itemsList = card.querySelector('.eetmoment-items');
|
const itemsList = card.querySelector('.eetmoment-items');
|
||||||
if (itemsList) {
|
if (itemsList) {
|
||||||
itemsList.classList.toggle('hidden');
|
itemsList.classList.toggle('collapsed');
|
||||||
const arrow = header.querySelector('span:last-child');
|
const arrow = header.querySelector('span:last-child');
|
||||||
if (arrow) {
|
if (arrow) {
|
||||||
arrow.innerHTML = itemsList.classList.contains('hidden') ? '<i class="fas fa-chevron-down"></i>' : '<i class="fas fa-chevron-up"></i>';
|
arrow.innerHTML = itemsList.classList.contains('collapsed') ? '<i class="fas fa-chevron-down"></i>' : '<i class="fas fa-chevron-up"></i>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -3568,6 +3665,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
// Checkbox change → update save/delete button state
|
// Checkbox change → update save/delete button state
|
||||||
listEl.querySelectorAll('.item-checkbox').forEach(cb => {
|
listEl.querySelectorAll('.item-checkbox').forEach(cb => {
|
||||||
cb.addEventListener('change', () => {
|
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');
|
const card = cb.closest('.eetmoment-card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const footer = card.querySelector('.save-meal-footer');
|
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 checked = card.querySelectorAll('.item-checkbox:checked').length;
|
||||||
const total = card.querySelectorAll('.item-checkbox').length;
|
const total = card.querySelectorAll('.item-checkbox').length;
|
||||||
if (checked < 2) {
|
if (checked < 2) {
|
||||||
saveBtn.style.display = 'none';
|
saveBtn.classList.add('footer-hidden');
|
||||||
if (delBtn) delBtn.classList.add('hidden');
|
if (delBtn) delBtn.classList.add('footer-hidden');
|
||||||
} else {
|
} else {
|
||||||
saveBtn.style.display = '';
|
saveBtn.classList.remove('footer-hidden');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.innerHTML = `<i class="fas fa-star"></i> Opslaan ${checked} items als maaltijd`;
|
saveBtn.innerHTML = `<i class='fas fa-star'></i> Opslaan ${checked} items als maaltijd`;
|
||||||
if (delBtn) delBtn.classList.remove('hidden');
|
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
|
// Bounce feedback
|
||||||
this.classList.add('tapped');
|
this.classList.add('tapped');
|
||||||
setTimeout(() => this.classList.remove('tapped'), 300);
|
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() {
|
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
|
// Bounce feedback
|
||||||
this.classList.add('tapped');
|
this.classList.add('tapped');
|
||||||
setTimeout(() => this.classList.remove('tapped'), 300);
|
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();
|
renderDagboek();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFromMeal(dateStr, momentId, index) {
|
async function removeFromMeal(dateStr, momentId, index, itemEl) {
|
||||||
const day = dagboek[dateStr];
|
const day = dagboek[dateStr];
|
||||||
if (day && day[momentId]) {
|
if (day && day[momentId]) {
|
||||||
const target = day[momentId][index];
|
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?`
|
message: `Weet je zeker dat je "${name}" wilt verwijderen?`
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
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);
|
day[momentId].splice(index, 1);
|
||||||
saveToStorage();
|
saveToStorage();
|
||||||
renderDagboek();
|
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.
|
* Remove a single item from within a meal group in the diary.
|
||||||
* If the last item is removed, removes the entire meal group.
|
* 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];
|
const day = dagboek[dateStr];
|
||||||
if (!day || !day[momentId]) return;
|
if (!day || !day[momentId]) return;
|
||||||
const entry = day[momentId][entryIdx];
|
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?`
|
message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?`
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
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);
|
entry.items.splice(mi, 1);
|
||||||
// If last item — remove the whole meal group entry
|
// If last item — remove the whole meal group entry
|
||||||
if (entry.items.length === 0) {
|
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.querySelector('#portieEditClose').addEventListener('click', closeDialog);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
||||||
|
setupSwipeDown(overlay, closeDialog);
|
||||||
|
|
||||||
const input = overlay.querySelector('#portieEditInput');
|
const input = overlay.querySelector('#portieEditInput');
|
||||||
const preview = overlay.querySelector('#portieEditPreview');
|
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('#bulkEditClose').addEventListener('click', closeDialog);
|
||||||
overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog);
|
overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
||||||
|
setupSwipeDown(overlay, closeDialog);
|
||||||
|
|
||||||
function collectPortions() {
|
function collectPortions() {
|
||||||
const portions = [];
|
const portions = [];
|
||||||
|
|
@ -4519,13 +4640,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
const input = overlay.querySelector('#mealNameInput');
|
const input = overlay.querySelector('#mealNameInput');
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
overlay.remove();
|
overlay.classList.add('closing');
|
||||||
document.body.style.overflow = '';
|
setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
const name = input.value.trim();
|
const name = input.value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
hapticError();
|
||||||
input.style.borderColor = 'var(--danger)';
|
input.style.borderColor = 'var(--danger)';
|
||||||
input.focus();
|
input.focus();
|
||||||
return;
|
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('#mealNameCancel').addEventListener('click', close);
|
||||||
overlay.querySelector('#mealNameSave').addEventListener('click', save);
|
overlay.querySelector('#mealNameSave').addEventListener('click', save);
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
// Enter key saves, Escape closes
|
// Enter key saves, Escape closes
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
|
|
@ -4584,7 +4707,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<div class="meal-card-name">${escapeHtml(meal.naam)}</div>
|
<div class="meal-card-name">${escapeHtml(meal.naam)}</div>
|
||||||
<div class="meal-card-meta">${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh</div>
|
<div class="meal-card-meta">${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="meal-card-delete" data-idx="${idx}" title="Verwijderen"><i class="fas fa-trash"></i></button>
|
<button class="meal-card-delete" data-haptic="strong" data-idx="${idx}" title="Verwijderen"><i class="fas fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="meal-card-add" data-idx="${idx}">
|
<div class="meal-card-add" data-idx="${idx}">
|
||||||
<i class="fas fa-plus-circle"></i> Toevoegen aan dagboek
|
<i class="fas fa-plus-circle"></i> Toevoegen aan dagboek
|
||||||
|
|
@ -4675,16 +4798,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
// Close handlers
|
// Close handlers
|
||||||
overlay.querySelector('.modal-close').addEventListener('click', () => {
|
function closeAddToMeal() {
|
||||||
overlay.remove();
|
overlay.classList.add('closing');
|
||||||
document.body.style.overflow = '';
|
setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280);
|
||||||
});
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
}
|
||||||
|
overlay.querySelector('.modal-close').addEventListener('click', closeAddToMeal);
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) closeAddToMeal();
|
||||||
});
|
});
|
||||||
|
setupSwipeDown(overlay, closeAddToMeal);
|
||||||
|
|
||||||
// Date picker navigation (event delegation on overlay)
|
// Date picker navigation (event delegation on overlay)
|
||||||
overlay.addEventListener('click', (e) => {
|
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 (portie > 0) newItems.push({ item: e.item, portie: portie });
|
||||||
});
|
});
|
||||||
if (newItems.length === 0) {
|
if (newItems.length === 0) {
|
||||||
|
hapticError();
|
||||||
showToast('Een maaltijd moet minimaal 1 item hebben');
|
showToast('Een maaltijd moet minimaal 1 item hebben');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -4820,6 +4943,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
overlay.querySelector('#mealDiaryClose').addEventListener('click', close);
|
overlay.querySelector('#mealDiaryClose').addEventListener('click', close);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -4844,6 +4968,91 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}, 250);
|
}, 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) {
|
function showToast(message) {
|
||||||
const container = document.getElementById('toastContainer');
|
const container = document.getElementById('toastContainer');
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
|
@ -4886,7 +5095,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:10px;padding:0 20px 20px;">
|
<div style="display:flex;gap:10px;padding:0 20px 20px;">
|
||||||
<button class="btn-cancel" style="flex:1;" id="confirmNo">Nee</button>
|
<button class="btn-cancel" style="flex:1;" id="confirmNo">Nee</button>
|
||||||
<button class="btn-danger-outline" style="flex:1;" id="confirmYes">${escapeHtml(confirmText)}</button>
|
<button class="btn-danger-outline" data-haptic="strong" style="flex:1;" id="confirmYes">${escapeHtml(confirmText)}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -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('#confirmNo').addEventListener('click', () => { close(); resolve(false); });
|
||||||
overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); });
|
overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); });
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } });
|
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) => {
|
overlay.addEventListener('click', (e) => {
|
||||||
if (e.target === overlay) close();
|
if (e.target === overlay) close();
|
||||||
});
|
});
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNameInputModal(title, defaultValue, onSave) {
|
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('#nameInputCancel').addEventListener('click', close);
|
||||||
overlay.querySelector('#nameInputSave').addEventListener('click', submit);
|
overlay.querySelector('#nameInputSave').addEventListener('click', submit);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
||||||
setTimeout(() => { input.focus(); input.select(); }, 100);
|
setTimeout(() => { input.focus(); input.select(); }, 100);
|
||||||
}
|
}
|
||||||
|
|
@ -4994,7 +5206,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<input type="number" class="portion-input" value="${entry.portie}" min="1" max="9999" data-item-idx="${i}" style="width:72px;min-width:72px;">
|
<input type="number" class="portion-input" value="${entry.portie}" min="1" max="9999" data-item-idx="${i}" style="width:72px;min-width:72px;">
|
||||||
<span class="portion-unit">g</span>
|
<span class="portion-unit">g</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="meal-edit-item-remove" data-item-idx="${i}" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="meal-edit-item-remove" data-haptic="strong" data-item-idx="${i}" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -5018,7 +5230,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</button>
|
</button>
|
||||||
<div class="meal-edit-actions">
|
<div class="meal-edit-actions">
|
||||||
<button class="btn-save" id="mealEditSave">Wijzigingen opslaan</button>
|
<button class="btn-save" id="mealEditSave">Wijzigingen opslaan</button>
|
||||||
<button class="btn-danger-outline" id="mealEditDelete">Maaltijd verwijderen</button>
|
<button class="btn-danger-outline" data-haptic="strong" id="mealEditDelete">Maaltijd verwijderen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5045,6 +5257,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (newItems.length === 0) {
|
if (newItems.length === 0) {
|
||||||
|
hapticError();
|
||||||
showToast('Een maaltijd moet minimaal 1 item hebben');
|
showToast('Een maaltijd moet minimaal 1 item hebben');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -5071,6 +5284,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
overlay.querySelector('#mealEditClose').addEventListener('click', close);
|
overlay.querySelector('#mealEditClose').addEventListener('click', close);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -5104,9 +5318,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(searchOverlay);
|
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.querySelector('.modal-close').addEventListener('click', closeSearch);
|
||||||
searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
|
searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
|
||||||
|
setupSwipeDown(searchOverlay, closeSearch);
|
||||||
const searchInput = searchOverlay.querySelector('#mealItemSearch');
|
const searchInput = searchOverlay.querySelector('#mealItemSearch');
|
||||||
const resultsEl = searchOverlay.querySelector('#mealSearchResults');
|
const resultsEl = searchOverlay.querySelector('#mealSearchResults');
|
||||||
searchInput.addEventListener('input', () => {
|
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('#mealPortionClose').addEventListener('click', closePicker);
|
||||||
po.querySelector('#mealPortionCancel').addEventListener('click', closePicker);
|
po.querySelector('#mealPortionCancel').addEventListener('click', closePicker);
|
||||||
po.addEventListener('click', (e) => { if (e.target === po) closePicker(); });
|
po.addEventListener('click', (e) => { if (e.target === po) closePicker(); });
|
||||||
|
setupSwipeDown(po, closePicker);
|
||||||
const input = po.querySelector('#mealPortionInput');
|
const input = po.querySelector('#mealPortionInput');
|
||||||
const preview = po.querySelector('#mealPortionPreview');
|
const preview = po.querySelector('#mealPortionPreview');
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
|
|
@ -5227,6 +5446,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
po.querySelector('#mealPortionConfirm').addEventListener('click', () => {
|
po.querySelector('#mealPortionConfirm').addEventListener('click', () => {
|
||||||
const portie = parseFloat(input.value) || 0;
|
const portie = parseFloat(input.value) || 0;
|
||||||
if (portie <= 0) {
|
if (portie <= 0) {
|
||||||
|
hapticError();
|
||||||
input.focus(); input.style.borderColor = 'var(--danger)';
|
input.focus(); input.style.borderColor = 'var(--danger)';
|
||||||
setTimeout(() => { input.style.borderColor = ''; }, 1500);
|
setTimeout(() => { input.style.borderColor = ''; }, 1500);
|
||||||
return;
|
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');
|
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
|
// Settings button & modal
|
||||||
document.getElementById('settingsBtn').addEventListener('click', () => {
|
document.getElementById('settingsBtn').addEventListener('click', () => {
|
||||||
// Populate with current limit
|
// 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');
|
if (e.target === document.getElementById('settingsModal')) closeModal('settingsModal');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupSwipeDown(document.getElementById('settingsModal'), () => closeModal('settingsModal'));
|
||||||
|
|
||||||
document.getElementById('saveCarbLimitBtn').addEventListener('click', () => {
|
document.getElementById('saveCarbLimitBtn').addEventListener('click', () => {
|
||||||
const input = document.getElementById('carbLimitInput');
|
const input = document.getElementById('carbLimitInput');
|
||||||
const val = parseInt(input.value, 10);
|
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 wrap = group.querySelector('.eetmoment-meal-items-wrap');
|
||||||
const actions = group.querySelector('.eetmoment-meal-actions');
|
const actions = group.querySelector('.eetmoment-meal-actions');
|
||||||
if (wrap) {
|
if (wrap) {
|
||||||
wrap.classList.toggle('hidden');
|
wrap.classList.toggle('collapsed');
|
||||||
if (actions) actions.classList.toggle('hidden');
|
if (actions) actions.classList.toggle('collapsed');
|
||||||
const arrow = btn.querySelector('span:last-child i');
|
const arrow = btn.querySelector('span:last-child i');
|
||||||
if (arrow) {
|
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();
|
renderDagboek();
|
||||||
});
|
});
|
||||||
overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); });
|
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
|
// Start the app
|
||||||
// Track last click position for toast positioning + haptic feedback
|
// Track last click position for toast positioning + haptic feedback
|
||||||
|
// Supports data-haptic="strong" (18ms) for destructive actions
|
||||||
(function setupHaptics() {
|
(function setupHaptics() {
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
lastClickY = e.clientY;
|
lastClickY = e.clientY;
|
||||||
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
||||||
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
|
const btn = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
|
||||||
navigator.vibrate(8);
|
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 });
|
}, { 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') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
335
index.html
335
index.html
|
|
@ -739,11 +739,15 @@ body {
|
||||||
background: var(--primary-light);
|
background: var(--primary-light);
|
||||||
}
|
}
|
||||||
.meal-group-items {
|
.meal-group-items {
|
||||||
display: none;
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
border-top: 1px solid var(--border);
|
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 {
|
.meal-group-header.expanded + .meal-group-items {
|
||||||
display: block;
|
max-height: 2000px;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.meal-sub-item {
|
.meal-sub-item {
|
||||||
padding-left: 36px;
|
padding-left: 36px;
|
||||||
|
|
@ -839,7 +843,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.eetmoment-items {
|
.eetmoment-items {
|
||||||
padding: 4px 0;
|
--collapsible-pt: 4px;
|
||||||
|
--collapsible-pb: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eetmoment-item {
|
.eetmoment-item {
|
||||||
|
|
@ -1282,11 +1287,23 @@ body {
|
||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
text-align: center;
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
min-height: var(--touch-min);
|
min-height: var(--touch-min);
|
||||||
line-height: 1.3;
|
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 {
|
.save-meal-footer-btn:hover {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
|
|
@ -1339,11 +1356,13 @@ body {
|
||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
text-align: center;
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
min-height: var(--touch-min);
|
min-height: var(--touch-min);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
max-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.delete-selected-footer-btn:hover {
|
.delete-selected-footer-btn:hover {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
|
|
@ -1353,8 +1372,17 @@ body {
|
||||||
background: #B71C1C;
|
background: #B71C1C;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
.delete-selected-footer-btn.hidden {
|
.delete-selected-footer-btn.hidden,
|
||||||
display: none;
|
.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 ===== */
|
/* ===== Diary — nested meal groups ===== */
|
||||||
|
|
@ -1391,7 +1419,8 @@ body {
|
||||||
}
|
}
|
||||||
.eetmoment-meal-items-wrap {
|
.eetmoment-meal-items-wrap {
|
||||||
border-top: 1px solid rgba(232, 183, 48, 0.2);
|
border-top: 1px solid rgba(232, 183, 48, 0.2);
|
||||||
padding: 2px 0;
|
--collapsible-pt: 2px;
|
||||||
|
--collapsible-pb: 2px;
|
||||||
}
|
}
|
||||||
.eetmoment-meal-item {
|
.eetmoment-meal-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -2185,6 +2214,11 @@ body {
|
||||||
@keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
|
@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 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 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; } }
|
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
||||||
|
|
||||||
/* -- Button press -- */
|
/* -- Button press -- */
|
||||||
|
|
@ -2284,6 +2318,58 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.5s ease-in-out infinite;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -2940,6 +3026,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
if (val !== null && (isNaN(val) || val < 0 || val > 30)) {
|
if (val !== null && (isNaN(val) || val < 0 || val > 30)) {
|
||||||
|
hapticError();
|
||||||
input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : '';
|
input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : '';
|
||||||
return;
|
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}"]`);
|
const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.outerHTML = renderInlineGlucoseItem(m);
|
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-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-item-kh">${formatKh(miKh)} g</span>
|
<span class="eetmoment-item-kh">${formatKh(miKh)} g</span>
|
||||||
<button class="eetmoment-remove" data-moment="${moment.id}" data-entry="${i}" data-mi="${mi}" aria-label="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="eetmoment-remove" data-haptic="strong" data-moment="${moment.id}" data-entry="${i}" data-mi="${mi}" aria-label="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -3422,15 +3517,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="eetmoment-meal-kh">${formatKh(mealKh)} g</span>
|
<span class="eetmoment-meal-kh">${formatKh(mealKh)} g</span>
|
||||||
<span style="font-size:0.7rem;color:var(--text-muted);"><i class="fas fa-chevron-up"></i></span>
|
<span style="font-size:0.7rem;color:var(--text-muted);"><i class="fas fa-chevron-up"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div>
|
<div class="eetmoment-meal-items-wrap collapsible">${mealItemsHtml}</div>
|
||||||
<div class="eetmoment-meal-actions">
|
<div class="eetmoment-meal-actions collapsible">
|
||||||
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-pen"></i> Porties
|
<i class="fas fa-pen"></i> Porties
|
||||||
</button>
|
</button>
|
||||||
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-arrow-right"></i> Verplaatsen
|
<i class="fas fa-arrow-right"></i> Verplaatsen
|
||||||
</button>
|
</button>
|
||||||
<button class="eetmoment-meal-action-btn eetmoment-delete" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
|
<button class="eetmoment-meal-action-btn eetmoment-delete" data-haptic="strong" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
|
||||||
<i class="fas fa-trash"></i> Verwijderen
|
<i class="fas fa-trash"></i> Verwijderen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3460,7 +3555,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-item-kh">${formatKh(itemKh)} g</span>
|
<span class="eetmoment-item-kh">${formatKh(itemKh)} g</span>
|
||||||
<button class="eetmoment-remove" data-moment="${moment.id}" data-index="${i}" aria-label="Verwijderen" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="eetmoment-remove" data-haptic="strong" data-moment="${moment.id}" data-index="${i}" aria-label="Verwijderen" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -3482,17 +3577,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
${items.length > 0 ? `<ul class="eetmoment-items collapsible">${itemsHtml}</ul>` : ''}
|
||||||
${inlineGlucoseHtml}
|
${inlineGlucoseHtml}
|
||||||
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
||||||
<span>+</span> Voedingsmiddel toevoegen
|
<span>+</span> Voedingsmiddel toevoegen
|
||||||
</div>
|
</div>
|
||||||
${items.length > 1 ? `
|
${items.length > 1 ? `
|
||||||
<div class="save-meal-footer" data-moment="${moment.id}">
|
<div class="save-meal-footer" data-moment="${moment.id}">
|
||||||
<button class="save-meal-footer-btn" data-moment="${moment.id}" style="display:none">
|
<button class="save-meal-footer-btn footer-hidden" data-moment="${moment.id}">
|
||||||
<i class="fas fa-star"></i> Selecteer ${items.length} items
|
<i class="fas fa-star"></i> Selecteer ${items.length} items
|
||||||
</button>
|
</button>
|
||||||
<button class="delete-selected-footer-btn hidden" data-moment="${moment.id}">
|
<button class="delete-selected-footer-btn footer-hidden" data-haptic="strong" data-moment="${moment.id}">
|
||||||
<i class="fas fa-trash"></i> Verwijder geselecteerde
|
<i class="fas fa-trash"></i> Verwijder geselecteerde
|
||||||
</button>
|
</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
@ -3502,7 +3597,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// Add event listeners — remove from diary
|
// Add event listeners — remove from diary
|
||||||
listEl.querySelectorAll('.eetmoment-remove').forEach(btn => {
|
listEl.querySelectorAll('.eetmoment-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const momentId = btn.dataset.moment;
|
const momentId = btn.dataset.moment;
|
||||||
const entry = parseInt(btn.dataset.index);
|
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;
|
const mi = btn.dataset.mi;
|
||||||
if (mi !== undefined) {
|
if (mi !== undefined) {
|
||||||
// Removing item from within a meal group
|
// 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 {
|
} 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 card = header.closest('.eetmoment-card');
|
||||||
const itemsList = card.querySelector('.eetmoment-items');
|
const itemsList = card.querySelector('.eetmoment-items');
|
||||||
if (itemsList) {
|
if (itemsList) {
|
||||||
itemsList.classList.toggle('hidden');
|
itemsList.classList.toggle('collapsed');
|
||||||
const arrow = header.querySelector('span:last-child');
|
const arrow = header.querySelector('span:last-child');
|
||||||
if (arrow) {
|
if (arrow) {
|
||||||
arrow.innerHTML = itemsList.classList.contains('hidden') ? '<i class="fas fa-chevron-down"></i>' : '<i class="fas fa-chevron-up"></i>';
|
arrow.innerHTML = itemsList.classList.contains('collapsed') ? '<i class="fas fa-chevron-down"></i>' : '<i class="fas fa-chevron-up"></i>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -3568,6 +3665,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
// Checkbox change → update save/delete button state
|
// Checkbox change → update save/delete button state
|
||||||
listEl.querySelectorAll('.item-checkbox').forEach(cb => {
|
listEl.querySelectorAll('.item-checkbox').forEach(cb => {
|
||||||
cb.addEventListener('change', () => {
|
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');
|
const card = cb.closest('.eetmoment-card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const footer = card.querySelector('.save-meal-footer');
|
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 checked = card.querySelectorAll('.item-checkbox:checked').length;
|
||||||
const total = card.querySelectorAll('.item-checkbox').length;
|
const total = card.querySelectorAll('.item-checkbox').length;
|
||||||
if (checked < 2) {
|
if (checked < 2) {
|
||||||
saveBtn.style.display = 'none';
|
saveBtn.classList.add('footer-hidden');
|
||||||
if (delBtn) delBtn.classList.add('hidden');
|
if (delBtn) delBtn.classList.add('footer-hidden');
|
||||||
} else {
|
} else {
|
||||||
saveBtn.style.display = '';
|
saveBtn.classList.remove('footer-hidden');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.innerHTML = `<i class="fas fa-star"></i> Opslaan ${checked} items als maaltijd`;
|
saveBtn.innerHTML = `<i class='fas fa-star'></i> Opslaan ${checked} items als maaltijd`;
|
||||||
if (delBtn) delBtn.classList.remove('hidden');
|
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
|
// Bounce feedback
|
||||||
this.classList.add('tapped');
|
this.classList.add('tapped');
|
||||||
setTimeout(() => this.classList.remove('tapped'), 300);
|
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() {
|
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
|
// Bounce feedback
|
||||||
this.classList.add('tapped');
|
this.classList.add('tapped');
|
||||||
setTimeout(() => this.classList.remove('tapped'), 300);
|
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();
|
renderDagboek();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFromMeal(dateStr, momentId, index) {
|
async function removeFromMeal(dateStr, momentId, index, itemEl) {
|
||||||
const day = dagboek[dateStr];
|
const day = dagboek[dateStr];
|
||||||
if (day && day[momentId]) {
|
if (day && day[momentId]) {
|
||||||
const target = day[momentId][index];
|
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?`
|
message: `Weet je zeker dat je "${name}" wilt verwijderen?`
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
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);
|
day[momentId].splice(index, 1);
|
||||||
saveToStorage();
|
saveToStorage();
|
||||||
renderDagboek();
|
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.
|
* Remove a single item from within a meal group in the diary.
|
||||||
* If the last item is removed, removes the entire meal group.
|
* 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];
|
const day = dagboek[dateStr];
|
||||||
if (!day || !day[momentId]) return;
|
if (!day || !day[momentId]) return;
|
||||||
const entry = day[momentId][entryIdx];
|
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?`
|
message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?`
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
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);
|
entry.items.splice(mi, 1);
|
||||||
// If last item — remove the whole meal group entry
|
// If last item — remove the whole meal group entry
|
||||||
if (entry.items.length === 0) {
|
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.querySelector('#portieEditClose').addEventListener('click', closeDialog);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
||||||
|
setupSwipeDown(overlay, closeDialog);
|
||||||
|
|
||||||
const input = overlay.querySelector('#portieEditInput');
|
const input = overlay.querySelector('#portieEditInput');
|
||||||
const preview = overlay.querySelector('#portieEditPreview');
|
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('#bulkEditClose').addEventListener('click', closeDialog);
|
||||||
overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog);
|
overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); });
|
||||||
|
setupSwipeDown(overlay, closeDialog);
|
||||||
|
|
||||||
function collectPortions() {
|
function collectPortions() {
|
||||||
const portions = [];
|
const portions = [];
|
||||||
|
|
@ -4519,13 +4640,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
const input = overlay.querySelector('#mealNameInput');
|
const input = overlay.querySelector('#mealNameInput');
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
overlay.remove();
|
overlay.classList.add('closing');
|
||||||
document.body.style.overflow = '';
|
setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
const name = input.value.trim();
|
const name = input.value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
hapticError();
|
||||||
input.style.borderColor = 'var(--danger)';
|
input.style.borderColor = 'var(--danger)';
|
||||||
input.focus();
|
input.focus();
|
||||||
return;
|
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('#mealNameCancel').addEventListener('click', close);
|
||||||
overlay.querySelector('#mealNameSave').addEventListener('click', save);
|
overlay.querySelector('#mealNameSave').addEventListener('click', save);
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
// Enter key saves, Escape closes
|
// Enter key saves, Escape closes
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
|
|
@ -4584,7 +4707,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<div class="meal-card-name">${escapeHtml(meal.naam)}</div>
|
<div class="meal-card-name">${escapeHtml(meal.naam)}</div>
|
||||||
<div class="meal-card-meta">${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh</div>
|
<div class="meal-card-meta">${itemCount} item${itemCount !== 1 ? 's' : ''} · ${meal.khTotaal} g kh</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="meal-card-delete" data-idx="${idx}" title="Verwijderen"><i class="fas fa-trash"></i></button>
|
<button class="meal-card-delete" data-haptic="strong" data-idx="${idx}" title="Verwijderen"><i class="fas fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="meal-card-add" data-idx="${idx}">
|
<div class="meal-card-add" data-idx="${idx}">
|
||||||
<i class="fas fa-plus-circle"></i> Toevoegen aan dagboek
|
<i class="fas fa-plus-circle"></i> Toevoegen aan dagboek
|
||||||
|
|
@ -4675,16 +4798,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
// Close handlers
|
// Close handlers
|
||||||
overlay.querySelector('.modal-close').addEventListener('click', () => {
|
function closeAddToMeal() {
|
||||||
overlay.remove();
|
overlay.classList.add('closing');
|
||||||
document.body.style.overflow = '';
|
setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280);
|
||||||
});
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
}
|
||||||
|
overlay.querySelector('.modal-close').addEventListener('click', closeAddToMeal);
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) closeAddToMeal();
|
||||||
});
|
});
|
||||||
|
setupSwipeDown(overlay, closeAddToMeal);
|
||||||
|
|
||||||
// Date picker navigation (event delegation on overlay)
|
// Date picker navigation (event delegation on overlay)
|
||||||
overlay.addEventListener('click', (e) => {
|
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 (portie > 0) newItems.push({ item: e.item, portie: portie });
|
||||||
});
|
});
|
||||||
if (newItems.length === 0) {
|
if (newItems.length === 0) {
|
||||||
|
hapticError();
|
||||||
showToast('Een maaltijd moet minimaal 1 item hebben');
|
showToast('Een maaltijd moet minimaal 1 item hebben');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -4820,6 +4943,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
overlay.querySelector('#mealDiaryClose').addEventListener('click', close);
|
overlay.querySelector('#mealDiaryClose').addEventListener('click', close);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -4844,6 +4968,91 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}, 250);
|
}, 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) {
|
function showToast(message) {
|
||||||
const container = document.getElementById('toastContainer');
|
const container = document.getElementById('toastContainer');
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
|
@ -4886,7 +5095,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:10px;padding:0 20px 20px;">
|
<div style="display:flex;gap:10px;padding:0 20px 20px;">
|
||||||
<button class="btn-cancel" style="flex:1;" id="confirmNo">Nee</button>
|
<button class="btn-cancel" style="flex:1;" id="confirmNo">Nee</button>
|
||||||
<button class="btn-danger-outline" style="flex:1;" id="confirmYes">${escapeHtml(confirmText)}</button>
|
<button class="btn-danger-outline" data-haptic="strong" style="flex:1;" id="confirmYes">${escapeHtml(confirmText)}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -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('#confirmNo').addEventListener('click', () => { close(); resolve(false); });
|
||||||
overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); });
|
overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); });
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } });
|
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) => {
|
overlay.addEventListener('click', (e) => {
|
||||||
if (e.target === overlay) close();
|
if (e.target === overlay) close();
|
||||||
});
|
});
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNameInputModal(title, defaultValue, onSave) {
|
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('#nameInputCancel').addEventListener('click', close);
|
||||||
overlay.querySelector('#nameInputSave').addEventListener('click', submit);
|
overlay.querySelector('#nameInputSave').addEventListener('click', submit);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
||||||
setTimeout(() => { input.focus(); input.select(); }, 100);
|
setTimeout(() => { input.focus(); input.select(); }, 100);
|
||||||
}
|
}
|
||||||
|
|
@ -4994,7 +5206,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<input type="number" class="portion-input" value="${entry.portie}" min="1" max="9999" data-item-idx="${i}" style="width:72px;min-width:72px;">
|
<input type="number" class="portion-input" value="${entry.portie}" min="1" max="9999" data-item-idx="${i}" style="width:72px;min-width:72px;">
|
||||||
<span class="portion-unit">g</span>
|
<span class="portion-unit">g</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="meal-edit-item-remove" data-item-idx="${i}" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
<button class="meal-edit-item-remove" data-haptic="strong" data-item-idx="${i}" title="Verwijderen"><i class="fas fa-xmark"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -5018,7 +5230,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</button>
|
</button>
|
||||||
<div class="meal-edit-actions">
|
<div class="meal-edit-actions">
|
||||||
<button class="btn-save" id="mealEditSave">Wijzigingen opslaan</button>
|
<button class="btn-save" id="mealEditSave">Wijzigingen opslaan</button>
|
||||||
<button class="btn-danger-outline" id="mealEditDelete">Maaltijd verwijderen</button>
|
<button class="btn-danger-outline" data-haptic="strong" id="mealEditDelete">Maaltijd verwijderen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5045,6 +5257,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (newItems.length === 0) {
|
if (newItems.length === 0) {
|
||||||
|
hapticError();
|
||||||
showToast('Een maaltijd moet minimaal 1 item hebben');
|
showToast('Een maaltijd moet minimaal 1 item hebben');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -5071,6 +5284,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
overlay.querySelector('#mealEditClose').addEventListener('click', close);
|
overlay.querySelector('#mealEditClose').addEventListener('click', close);
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||||
|
setupSwipeDown(overlay, close);
|
||||||
|
|
||||||
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
overlay.querySelectorAll('.meal-edit-item-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -5104,9 +5318,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(searchOverlay);
|
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.querySelector('.modal-close').addEventListener('click', closeSearch);
|
||||||
searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
|
searchOverlay.addEventListener('click', (e) => { if (e.target === searchOverlay) closeSearch(); });
|
||||||
|
setupSwipeDown(searchOverlay, closeSearch);
|
||||||
const searchInput = searchOverlay.querySelector('#mealItemSearch');
|
const searchInput = searchOverlay.querySelector('#mealItemSearch');
|
||||||
const resultsEl = searchOverlay.querySelector('#mealSearchResults');
|
const resultsEl = searchOverlay.querySelector('#mealSearchResults');
|
||||||
searchInput.addEventListener('input', () => {
|
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('#mealPortionClose').addEventListener('click', closePicker);
|
||||||
po.querySelector('#mealPortionCancel').addEventListener('click', closePicker);
|
po.querySelector('#mealPortionCancel').addEventListener('click', closePicker);
|
||||||
po.addEventListener('click', (e) => { if (e.target === po) closePicker(); });
|
po.addEventListener('click', (e) => { if (e.target === po) closePicker(); });
|
||||||
|
setupSwipeDown(po, closePicker);
|
||||||
const input = po.querySelector('#mealPortionInput');
|
const input = po.querySelector('#mealPortionInput');
|
||||||
const preview = po.querySelector('#mealPortionPreview');
|
const preview = po.querySelector('#mealPortionPreview');
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
|
|
@ -5227,6 +5446,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
po.querySelector('#mealPortionConfirm').addEventListener('click', () => {
|
po.querySelector('#mealPortionConfirm').addEventListener('click', () => {
|
||||||
const portie = parseFloat(input.value) || 0;
|
const portie = parseFloat(input.value) || 0;
|
||||||
if (portie <= 0) {
|
if (portie <= 0) {
|
||||||
|
hapticError();
|
||||||
input.focus(); input.style.borderColor = 'var(--danger)';
|
input.focus(); input.style.borderColor = 'var(--danger)';
|
||||||
setTimeout(() => { input.style.borderColor = ''; }, 1500);
|
setTimeout(() => { input.style.borderColor = ''; }, 1500);
|
||||||
return;
|
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');
|
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
|
// Settings button & modal
|
||||||
document.getElementById('settingsBtn').addEventListener('click', () => {
|
document.getElementById('settingsBtn').addEventListener('click', () => {
|
||||||
// Populate with current limit
|
// 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');
|
if (e.target === document.getElementById('settingsModal')) closeModal('settingsModal');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupSwipeDown(document.getElementById('settingsModal'), () => closeModal('settingsModal'));
|
||||||
|
|
||||||
document.getElementById('saveCarbLimitBtn').addEventListener('click', () => {
|
document.getElementById('saveCarbLimitBtn').addEventListener('click', () => {
|
||||||
const input = document.getElementById('carbLimitInput');
|
const input = document.getElementById('carbLimitInput');
|
||||||
const val = parseInt(input.value, 10);
|
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 wrap = group.querySelector('.eetmoment-meal-items-wrap');
|
||||||
const actions = group.querySelector('.eetmoment-meal-actions');
|
const actions = group.querySelector('.eetmoment-meal-actions');
|
||||||
if (wrap) {
|
if (wrap) {
|
||||||
wrap.classList.toggle('hidden');
|
wrap.classList.toggle('collapsed');
|
||||||
if (actions) actions.classList.toggle('hidden');
|
if (actions) actions.classList.toggle('collapsed');
|
||||||
const arrow = btn.querySelector('span:last-child i');
|
const arrow = btn.querySelector('span:last-child i');
|
||||||
if (arrow) {
|
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();
|
renderDagboek();
|
||||||
});
|
});
|
||||||
overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); });
|
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
|
// Start the app
|
||||||
// Track last click position for toast positioning + haptic feedback
|
// Track last click position for toast positioning + haptic feedback
|
||||||
|
// Supports data-haptic="strong" (18ms) for destructive actions
|
||||||
(function setupHaptics() {
|
(function setupHaptics() {
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
lastClickY = e.clientY;
|
lastClickY = e.clientY;
|
||||||
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
||||||
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
|
const btn = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
|
||||||
navigator.vibrate(8);
|
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 });
|
}, { 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') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue