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:
parent
c2b9074ef9
commit
9c3a07bbce
2 changed files with 234 additions and 16 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
123
index.html
123
index.html
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue