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 }) {
-
+
- ${t('calendar.allDayToggle')}
+
+ ${t('calendar.allDayToggle')}
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 }) {
-
+
- ${t('notes.pinnedLabel')}
+
+ ${t('notes.pinnedLabel')}
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; }