diff --git a/public/pages/budget.js b/public/pages/budget.js index 7766f8c..17e4cbb 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -377,9 +377,10 @@ function openBudgetModal({ mode, entry = null }) {
-
diff --git a/public/pages/calendar.js b/public/pages/calendar.js index 2b898f3..8429227 100644 --- a/public/pages/calendar.js +++ b/public/pages/calendar.js @@ -795,9 +795,10 @@ function buildEventModalContent({ mode, event, date }) {
-
diff --git a/public/pages/notes.js b/public/pages/notes.js index f594584..e830bf8 100644 --- a/public/pages/notes.js +++ b/public/pages/notes.js @@ -379,9 +379,10 @@ function openNoteModal({ mode, note = null }) {
-
diff --git a/public/pages/shopping.js b/public/pages/shopping.js index 2f67d59..bbef484 100644 --- a/public/pages/shopping.js +++ b/public/pages/shopping.js @@ -350,6 +350,7 @@ function wireSwipeGestures(container) { let startX = 0, startY = 0; let dx = 0; let locked = false; // false | 'swipe' | 'scroll' + let thresholdHit = false; const card = row.querySelector('.shopping-item'); if (!card) return; @@ -367,6 +368,7 @@ function wireSwipeGestures(container) { startY = e.touches[0].clientY; dx = 0; locked = false; + thresholdHit = false; card.style.transition = ''; }, { passive: true }); @@ -408,6 +410,12 @@ function wireSwipeGestures(container) { row.querySelector('.swipe-reveal--delete').style.opacity = String(progress); row.querySelector('.swipe-reveal--done').style.opacity = '0'; } + + // Haptic-Feedback beim Erreichen des Schwellwerts + if (!thresholdHit && Math.abs(dx) >= SWIPE_THRESHOLD) { + thresholdHit = true; + vibrate(15); + } }, { passive: false }); row.addEventListener('touchend', async () => { diff --git a/public/pages/tasks.js b/public/pages/tasks.js index 09c9d32..6a28e66 100644 --- a/public/pages/tasks.js +++ b/public/pages/tasks.js @@ -730,6 +730,7 @@ function wireSwipeGestures(container) { let startX = 0, startY = 0; let dx = 0; let locked = false; // false = unentschieden, 'swipe' | 'scroll' + let thresholdHit = false; // Haptic-Feedback am Threshold nur einmal const card = row.querySelector('.task-card'); if (!card) return; @@ -749,6 +750,7 @@ function wireSwipeGestures(container) { startY = e.touches[0].clientY; dx = 0; locked = false; + thresholdHit = false; card.style.transition = ''; }, { passive: true }); @@ -794,6 +796,12 @@ function wireSwipeGestures(container) { row.querySelector('.swipe-reveal--edit').style.opacity = String(progress); row.querySelector('.swipe-reveal--done').style.opacity = '0'; } + + // Haptic-Feedback beim Erreichen des Schwellwerts + if (!thresholdHit && Math.abs(dx) >= SWIPE_THRESHOLD) { + thresholdHit = true; + vibrate(15); + } }, { passive: false }); row.addEventListener('touchend', async () => { diff --git a/public/router.js b/public/router.js index 6f9b59c..1b0b079 100644 --- a/public/router.js +++ b/public/router.js @@ -391,15 +391,27 @@ function renderError(container, err) { * @param {'default'|'success'|'danger'|'warning'} type * @param {number} duration - ms */ +const TOAST_ICONS = { + success: '', + danger: '', + warning: '', +}; + function showToast(message, type = 'default', duration = 3000) { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type !== 'default' ? `toast--${type}` : ''}`; - toast.textContent = message; toast.setAttribute('role', 'alert'); + // Icon: statische SVGs aus TOAST_ICONS (kein User-Input, kein XSS-Risiko) + const icon = TOAST_ICONS[type] || ''; + const span = document.createElement('span'); + span.textContent = message; + toast.innerHTML = icon; // eslint-disable-line no-unsanitized/property -- static SVG only + toast.appendChild(span); + container.appendChild(toast); setTimeout(() => { toast.classList.add('toast--out'); diff --git a/public/styles/calendar.css b/public/styles/calendar.css index 1a04153..f1d541d 100644 --- a/public/styles/calendar.css +++ b/public/styles/calendar.css @@ -555,19 +555,6 @@ transform: scale(1.1); } -/* Allday-Toggle */ -.allday-toggle { - display: flex; - align-items: center; - gap: var(--space-3); - cursor: pointer; -} - -.allday-toggle__label { - font-size: var(--text-sm); - color: var(--color-text-primary); -} - /* -------------------------------------------------------- * Termin-Detailansicht (Popup beim Klick) * -------------------------------------------------------- */ diff --git a/public/styles/layout.css b/public/styles/layout.css index 2f73859..42c5c49 100644 --- a/public/styles/layout.css +++ b/public/styles/layout.css @@ -179,7 +179,7 @@ gap: 2px; padding: var(--space-1) var(--space-1); color: var(--color-text-tertiary); - transition: color var(--transition-fast); + transition: color var(--transition-fast), transform 0.12s ease; -webkit-tap-highlight-color: transparent; min-height: var(--target-lg); min-width: var(--target-lg); @@ -188,6 +188,7 @@ .nav-item:active { transform: scale(0.92); + transition-duration: 0.06s; } /* ── Nav-Item: Aktiv ── */ @@ -242,6 +243,11 @@ transform: scale(0.92); } +.page-fab:focus-visible { + outline: 2px solid #fff; + outline-offset: 2px; +} + /* Desktop: FAB Position anpassen (keine Bottom-Nav) und etwas kleiner */ @media (min-width: 1024px) { .page-fab { @@ -557,6 +563,11 @@ transform: scale(0.99); } +.card--interactive:focus-visible { + outline: 2px solid var(--active-module-accent, var(--color-accent)); + outline-offset: 2px; +} + @media (min-width: 1024px) { .card--padded { padding: var(--space-5); @@ -773,6 +784,11 @@ transform: scale(0.98); } +.btn:focus-visible { + outline: 2px solid var(--active-module-accent, var(--color-accent)); + outline-offset: 2px; +} + .btn--primary { background-color: var(--color-btn-primary); color: #ffffff; @@ -864,6 +880,11 @@ transform: scale(0.95); } +.fab:focus-visible { + outline: 2px solid #fff; + outline-offset: 2px; +} + @media (min-width: 1024px) { .fab { bottom: var(--space-8); @@ -961,6 +982,74 @@ display: flex; } +/* -------------------------------------------------------- + * Toggle-Switch + * Custom iOS-style toggle, ersetzt native Checkboxen. + * Usage: + * -------------------------------------------------------- */ +.toggle { + display: inline-flex; + align-items: center; + cursor: pointer; + gap: var(--space-3); +} + +.toggle input { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); +} + +.toggle__track { + position: relative; + width: 44px; + height: 26px; + background-color: var(--neutral-300); + border-radius: var(--radius-full); + transition: background-color 0.2s ease; + flex-shrink: 0; +} + +.toggle__track::after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + background: #fff; + border-radius: var(--radius-full); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.2s var(--ease-out); +} + +.toggle input:checked + .toggle__track { + background-color: var(--active-module-accent, var(--color-accent)); +} + +.toggle input:checked + .toggle__track::after { + transform: translateX(18px); +} + +.toggle input:focus-visible + .toggle__track { + outline: 2px solid var(--active-module-accent, var(--color-accent)); + outline-offset: 2px; +} + +.toggle input:disabled + .toggle__track { + opacity: 0.4; + cursor: not-allowed; +} + +@media (prefers-reduced-motion: reduce) { + .toggle__track, + .toggle__track::after { + transition: none; + } +} + /* -------------------------------------------------------- * Skeleton-Loading * -------------------------------------------------------- */ @@ -992,6 +1081,16 @@ gap: var(--space-3); padding: var(--space-12) var(--space-6); text-align: center; + animation: empty-state-in 0.4s var(--ease-out) both; +} + +@keyframes empty-state-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (prefers-reduced-motion: reduce) { + .empty-state { animation: none; } } .empty-state__icon { @@ -1212,6 +1311,9 @@ } .toast { + display: flex; + align-items: center; + gap: var(--space-2); background-color: var(--neutral-800); color: var(--neutral-50); padding: var(--space-3) var(--space-4); @@ -1222,6 +1324,12 @@ animation: toast-in 0.25s var(--ease-out) forwards; } +.toast__icon { + flex-shrink: 0; + width: 16px; + height: 16px; +} + .toast--success { background-color: var(--color-success); color: #fff; } .toast--danger { background-color: var(--color-danger); color: #fff; } .toast--warning { background-color: var(--color-warning); color: #fff; }