feat: animatiesysteem voor alle interacties (knoppen, modals, tabs, toasts, lijsten)

- 9 @keyframes: fadeIn/Out, slideUp/Down, slideInRight/OutRight, scaleIn, pulse, shimmer
- Button press: scale(0.96) op :active met 0.12s transition
- Modals: slideUp open (0.3s cubic-bezier), slideDown close (0.25s)
- Toast: slideInRight open, slideOutRight dismiss na 2s
- Tabs: crossfade switching met .switching class
- List items: staggered fadeIn via --i CSS variable (50ms per item)
- Stepper: pulse animatie op +/− tap
- Chips/pills: scale feedback op :active
- Diary entries: fadeIn + slideUp op nieuwe entries
This commit is contained in:
cas 2026-07-24 01:57:06 +02:00
parent c2b9074ef9
commit 9c3a07bbce
2 changed files with 234 additions and 16 deletions

View file

@ -1920,6 +1920,97 @@ body {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
} }
/* ===== Animation System ===== */
/* -- Keyframes -- */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes slideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
@keyframes slideInRight { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
@keyframes scaleIn { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.12); } }
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
/* -- Button press -- */
button, .btn, .meal-card-main, .dagboek-item, .food-item,
.standaard-portie-btn, .portie-stepper-plus, .portie-stepper-minus {
transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
}
button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-item:active,
.portie-stepper-plus:active, .portie-stepper-minus:active {
transform: scale(0.96);
}
/* -- Modal / Bottom Sheet -- */
.modal-overlay {
animation: fadeIn 0.2s ease both;
}
.modal-overlay.closing {
animation: fadeOut 0.2s ease both;
}
.modal-sheet {
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.modal-overlay.closing .modal-sheet {
animation: slideDown 0.25s ease-in both;
}
/* -- Tab content transition -- */
.tab-content {
animation: fadeIn 0.25s ease both;
}
.tab-content.switching {
animation: fadeIn 0.2s ease both;
}
/* -- Staggered list fade-in -- */
.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item {
animation: fadeIn 0.3s ease both;
animation-delay: calc(var(--i, 0) * 50ms);
}
/* -- Toast notification -- */
.toast {
animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.toast.hiding {
animation: slideOutRight 0.3s ease-in both;
}
/* -- Stepper -- */
.portie-stepper.active {
transition: background 0.2s ease, border-color 0.2s ease;
}
.portie-stepper-plus.tapped, .portie-stepper-minus.tapped {
animation: pulse 0.3s ease;
}
/* -- Chip / Pill select -- */
.chip, .categorie-chip, .tab-chip {
transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease;
}
.chip:active, .categorie-chip:active, .tab-chip:active {
transform: scale(0.93);
}
/* -- Diary entry add animation -- */
.eetmoment-entry {
animation: fadeIn 0.3s ease both, slideUp 0.3s ease both;
}
/* -- Page transitions between main views -- */
.page-slide-right { animation: slideInRight 0.3s ease both; }
.page-fade-in { animation: fadeIn 0.3s ease both; }
/* -- Loading skeleton shimmer -- */
.skeleton {
background: linear-gradient(90deg, var(--border) 25%, #e8e8e8 50%, var(--border) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
</style> </style>
</head> </head>
<body> <body>
@ -2608,10 +2699,10 @@ body {
emptyEl.classList.remove('hidden'); emptyEl.classList.remove('hidden');
} else { } else {
emptyEl.classList.add('hidden'); emptyEl.classList.add('hidden');
listEl.innerHTML = items.map(item => { listEl.innerHTML = items.map((item, idx) => {
const kh = parseKh(item.kh); const kh = parseKh(item.kh);
return ` return `
<li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" role="button" tabindex="0"> <li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" style="--i:${idx}" role="button" tabindex="0">
<div class="food-item-left"> <div class="food-item-left">
<div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div> <div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div>
<div class="food-item-cat">${escapeHtml(item.cat)}</div> <div class="food-item-cat">${escapeHtml(item.cat)}</div>
@ -3443,9 +3534,18 @@ body {
function switchTab(tab) { function switchTab(tab) {
activeTab = tab; activeTab = tab;
document.getElementById('tabZoeken').classList.toggle('hidden', tab !== 'zoeken'); const panels = ['tabZoeken','tabDagboek','tabMaaltijden'];
document.getElementById('tabDagboek').classList.toggle('hidden', tab !== 'dagboek'); panels.forEach(id => {
document.getElementById('tabMaaltijden').classList.toggle('hidden', tab !== 'maaltijden'); const el = document.getElementById(id);
if (id === 'tab' + tab.charAt(0).toUpperCase() + tab.slice(1)) {
el.classList.add('switching');
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
el.classList.remove('switching');
}
});
document.getElementById('searchContainer').classList.toggle('hidden', tab !== 'zoeken'); document.getElementById('searchContainer').classList.toggle('hidden', tab !== 'zoeken');
document.getElementById('categoryFilter').classList.toggle('hidden', tab !== 'zoeken'); document.getElementById('categoryFilter').classList.toggle('hidden', tab !== 'zoeken');
@ -4025,8 +4125,14 @@ body {
// ===== Modal Helpers ===== // ===== Modal Helpers =====
function closeModal(modalId) { function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden'); const el = document.getElementById(modalId);
if (!el) return;
el.classList.add('closing');
setTimeout(() => {
el.classList.add('hidden');
el.classList.remove('closing');
document.body.style.overflow = ''; document.body.style.overflow = '';
}, 250);
} }
function showToast(message) { function showToast(message) {
@ -4035,7 +4141,10 @@ body {
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
setTimeout(() => toast.remove(), 2000); setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
}, 2000);
} }
function showNameInputModal(title, defaultValue, onSave) { function showNameInputModal(title, defaultValue, onSave) {

View file

@ -1920,6 +1920,97 @@ body {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
} }
/* ===== Animation System ===== */
/* -- Keyframes -- */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes slideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
@keyframes slideInRight { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes slideOutRight{ from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
@keyframes scaleIn { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.12); } }
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
/* -- Button press -- */
button, .btn, .meal-card-main, .dagboek-item, .food-item,
.standaard-portie-btn, .portie-stepper-plus, .portie-stepper-minus {
transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
}
button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-item:active,
.portie-stepper-plus:active, .portie-stepper-minus:active {
transform: scale(0.96);
}
/* -- Modal / Bottom Sheet -- */
.modal-overlay {
animation: fadeIn 0.2s ease both;
}
.modal-overlay.closing {
animation: fadeOut 0.2s ease both;
}
.modal-sheet {
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.modal-overlay.closing .modal-sheet {
animation: slideDown 0.25s ease-in both;
}
/* -- Tab content transition -- */
.tab-content {
animation: fadeIn 0.25s ease both;
}
.tab-content.switching {
animation: fadeIn 0.2s ease both;
}
/* -- Staggered list fade-in -- */
.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item {
animation: fadeIn 0.3s ease both;
animation-delay: calc(var(--i, 0) * 50ms);
}
/* -- Toast notification -- */
.toast {
animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.toast.hiding {
animation: slideOutRight 0.3s ease-in both;
}
/* -- Stepper -- */
.portie-stepper.active {
transition: background 0.2s ease, border-color 0.2s ease;
}
.portie-stepper-plus.tapped, .portie-stepper-minus.tapped {
animation: pulse 0.3s ease;
}
/* -- Chip / Pill select -- */
.chip, .categorie-chip, .tab-chip {
transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease;
}
.chip:active, .categorie-chip:active, .tab-chip:active {
transform: scale(0.93);
}
/* -- Diary entry add animation -- */
.eetmoment-entry {
animation: fadeIn 0.3s ease both, slideUp 0.3s ease both;
}
/* -- Page transitions between main views -- */
.page-slide-right { animation: slideInRight 0.3s ease both; }
.page-fade-in { animation: fadeIn 0.3s ease both; }
/* -- Loading skeleton shimmer -- */
.skeleton {
background: linear-gradient(90deg, var(--border) 25%, #e8e8e8 50%, var(--border) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
</style> </style>
</head> </head>
<body> <body>
@ -2608,10 +2699,10 @@ body {
emptyEl.classList.remove('hidden'); emptyEl.classList.remove('hidden');
} else { } else {
emptyEl.classList.add('hidden'); emptyEl.classList.add('hidden');
listEl.innerHTML = items.map(item => { listEl.innerHTML = items.map((item, idx) => {
const kh = parseKh(item.kh); const kh = parseKh(item.kh);
return ` return `
<li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" role="button" tabindex="0"> <li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" style="--i:${idx}" role="button" tabindex="0">
<div class="food-item-left"> <div class="food-item-left">
<div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div> <div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div>
<div class="food-item-cat">${escapeHtml(item.cat)}</div> <div class="food-item-cat">${escapeHtml(item.cat)}</div>
@ -3443,9 +3534,18 @@ body {
function switchTab(tab) { function switchTab(tab) {
activeTab = tab; activeTab = tab;
document.getElementById('tabZoeken').classList.toggle('hidden', tab !== 'zoeken'); const panels = ['tabZoeken','tabDagboek','tabMaaltijden'];
document.getElementById('tabDagboek').classList.toggle('hidden', tab !== 'dagboek'); panels.forEach(id => {
document.getElementById('tabMaaltijden').classList.toggle('hidden', tab !== 'maaltijden'); const el = document.getElementById(id);
if (id === 'tab' + tab.charAt(0).toUpperCase() + tab.slice(1)) {
el.classList.add('switching');
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
el.classList.remove('switching');
}
});
document.getElementById('searchContainer').classList.toggle('hidden', tab !== 'zoeken'); document.getElementById('searchContainer').classList.toggle('hidden', tab !== 'zoeken');
document.getElementById('categoryFilter').classList.toggle('hidden', tab !== 'zoeken'); document.getElementById('categoryFilter').classList.toggle('hidden', tab !== 'zoeken');
@ -4025,8 +4125,14 @@ body {
// ===== Modal Helpers ===== // ===== Modal Helpers =====
function closeModal(modalId) { function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden'); const el = document.getElementById(modalId);
if (!el) return;
el.classList.add('closing');
setTimeout(() => {
el.classList.add('hidden');
el.classList.remove('closing');
document.body.style.overflow = ''; document.body.style.overflow = '';
}, 250);
} }
function showToast(message) { function showToast(message) {
@ -4035,7 +4141,10 @@ body {
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
setTimeout(() => toast.remove(), 2000); setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
}, 2000);
} }
function showNameInputModal(title, defaultValue, onSave) { function showNameInputModal(title, defaultValue, onSave) {