feat(ux): microinteraction polish — undo tap feedback, strikethrough transition, modal loading state

- toast__undo: add :active scale + tap-highlight-color for reliable tap feedback
- task titles: animate text-decoration-color instead of snapping for smoother done-state
- modal forms: auto-add btn--loading on submit; rAF guard removes it on validation fail;
  MutationObserver removes it on error re-enable; btnSuccess clears it before checkmark

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-04-27 00:20:42 +02:00
parent e8104adb1e
commit 201fa05afd
3 changed files with 33 additions and 1 deletions
+17
View File
@@ -294,6 +294,22 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}
// Callback für Aufrufer (Form-Events binden etc.) // Callback für Aufrufer (Form-Events binden etc.)
if (typeof onSave === 'function') onSave(panel); if (typeof onSave === 'function') onSave(panel);
// Loading-State: btn--loading auf Submit-Button während async-Save.
// rAF-Check: Validierung schlägt fehl → btn bleibt enabled → Loading sofort entfernen.
// MutationObserver: Error-Pfad → btn wird re-enabled → Loading entfernen.
panel.addEventListener('submit', (e) => {
const btn = e.target.querySelector('[type="submit"], .btn--primary');
if (!btn || btn.disabled) return;
btn.classList.add('btn--loading');
requestAnimationFrame(() => {
if (!btn.disabled) { btn.classList.remove('btn--loading'); return; }
const mo = new MutationObserver(() => {
if (!btn.disabled) { btn.classList.remove('btn--loading'); mo.disconnect(); }
});
mo.observe(btn, { attributes: true, attributeFilter: ['disabled'] });
});
}, { capture: true });
// Standalone: Statusbar abdunkeln (Overlay-Effekt) // Standalone: Statusbar abdunkeln (Overlay-Effekt)
if (window.oikos?.setThemeColor) { if (window.oikos?.setThemeColor) {
window.oikos.setThemeColor(OVERLAY_THEME_COLOR, OVERLAY_THEME_COLOR); window.oikos.setThemeColor(OVERLAY_THEME_COLOR, OVERLAY_THEME_COLOR);
@@ -620,6 +636,7 @@ export function validateAll(formContainer) {
* @param {string} [originalLabel] * @param {string} [originalLabel]
*/ */
export function btnSuccess(btn, originalLabel) { export function btnSuccess(btn, originalLabel) {
btn.classList.remove('btn--loading');
const label = originalLabel ?? btn.textContent; const label = originalLabel ?? btn.textContent;
btn.classList.add('btn--success'); btn.classList.add('btn--success');
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
+10
View File
@@ -1647,10 +1647,20 @@
opacity: 0.85; opacity: 0.85;
} }
.toast__undo {
-webkit-tap-highlight-color: transparent;
transition: opacity var(--transition-fast), transform 0.08s ease;
}
.toast__undo:hover { .toast__undo:hover {
opacity: 1; opacity: 1;
} }
.toast__undo:active {
transform: scale(0.94);
opacity: 1;
}
.toast--success { background-color: var(--color-success); color: var(--toast-success-text); } .toast--success { background-color: var(--color-success); color: var(--toast-success-text); }
.toast--danger { background-color: var(--color-danger); color: var(--toast-danger-text); } .toast--danger { background-color: var(--color-danger); color: var(--toast-danger-text); }
.toast--warning { background-color: var(--color-warning); color: var(--toast-warning-text); } .toast--warning { background-color: var(--color-warning); color: var(--toast-warning-text); }
+6 -1
View File
@@ -406,11 +406,16 @@
color: var(--color-text-primary); color: var(--color-text-primary);
margin-bottom: var(--space-1); margin-bottom: var(--space-1);
cursor: pointer; cursor: pointer;
text-decoration: line-through;
text-decoration-color: transparent;
transition:
color var(--transition-fast),
text-decoration-color var(--transition-base);
} }
.task-card--done .task-card__title { .task-card--done .task-card__title {
text-decoration: line-through;
color: var(--color-text-secondary); color: var(--color-text-secondary);
text-decoration-color: var(--color-text-secondary);
} }
.task-card__meta { .task-card__meta {