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.)
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)
if (window.oikos?.setThemeColor) {
window.oikos.setThemeColor(OVERLAY_THEME_COLOR, OVERLAY_THEME_COLOR);
@@ -620,6 +636,7 @@ export function validateAll(formContainer) {
* @param {string} [originalLabel]
*/
export function btnSuccess(btn, originalLabel) {
btn.classList.remove('btn--loading');
const label = originalLabel ?? btn.textContent;
btn.classList.add('btn--success');
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;