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:
@@ -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;
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user