From bd0dbaf91d6eb904bb05d6d03bbd3177f458e0f3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 11:22:32 +0200 Subject: [PATCH] feat: add animation CSS classes, keyframes, and reduced-motion support --- src/styles/main.css | 209 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 2 deletions(-) diff --git a/src/styles/main.css b/src/styles/main.css index 7b08f65..66bd000 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -196,11 +196,11 @@ @keyframes toast-enter { from { opacity: 0; - transform: translateY(-20px); + transform: translateY(-20px) translateX(10px); } to { opacity: 1; - transform: translateY(0); + transform: translateY(0) translateX(0); } } @@ -211,9 +211,11 @@ @keyframes toast-exit { from { opacity: 1; + transform: translateY(0); } to { opacity: 0; + transform: translateY(-10px); } } @@ -245,3 +247,206 @@ color: var(--color-accent-text); text-decoration: underline; } + +/* ============================================ + MOTION SYSTEM — Transitions & Animations + ============================================ */ + +/* Page transitions */ +.page-enter-active { + transition: opacity 250ms cubic-bezier(0.22, 1, 0.36, 1), + transform 250ms cubic-bezier(0.22, 1, 0.36, 1); +} +.page-leave-active { + transition: opacity 150ms ease-out, + transform 150ms ease-out; +} +.page-enter-from { + opacity: 0; + transform: translateY(8px); +} +.page-leave-to { + opacity: 0; + transform: translateY(-8px); +} + +/* List item transitions */ +.list-enter-active { + transition: opacity 250ms cubic-bezier(0.22, 1, 0.36, 1), + transform 250ms cubic-bezier(0.22, 1, 0.36, 1); +} +.list-leave-active { + transition: opacity 150ms ease-in, + transform 150ms ease-in; + position: absolute; +} +.list-move { + transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1); +} +.list-enter-from { + opacity: 0; + transform: translateY(12px); +} +.list-leave-to { + opacity: 0; + transform: translateX(-20px); +} + +/* Tag chip transitions */ +.chip-enter-active { + transition: opacity 200ms cubic-bezier(0.34, 1.56, 0.64, 1), + transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1); +} +.chip-leave-active { + transition: opacity 100ms ease-in, + transform 100ms ease-in; +} +.chip-enter-from { + opacity: 0; + transform: scale(0.8); +} +.chip-leave-to { + opacity: 0; + transform: scale(0.8); +} + +/* Modal transitions */ +.modal-enter-active { + transition: opacity 200ms ease-out; +} +.modal-leave-active { + transition: opacity 150ms ease-in; +} +.modal-enter-from, +.modal-leave-to { + opacity: 0; +} + +/* Dropdown transitions */ +.dropdown-enter-active { + transition: opacity 150ms cubic-bezier(0.22, 1, 0.36, 1), + transform 150ms cubic-bezier(0.22, 1, 0.36, 1); +} +.dropdown-leave-active { + transition: opacity 100ms ease-in, + transform 100ms ease-in; +} +.dropdown-enter-from { + opacity: 0; + transform: translateY(-4px) scale(0.95); +} +.dropdown-leave-to { + opacity: 0; + transform: translateY(-4px); +} + +/* Content fade-in */ +.fade-enter-active { + transition: opacity 250ms cubic-bezier(0.22, 1, 0.36, 1), + transform 250ms cubic-bezier(0.22, 1, 0.36, 1); +} +.fade-leave-active { + transition: opacity 150ms ease-out; +} +.fade-enter-from { + opacity: 0; + transform: translateY(4px); +} +.fade-leave-to { + opacity: 0; +} + +/* Skeleton shimmer */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton { + background: linear-gradient(90deg, var(--color-bg-elevated) 25%, var(--color-bg-surface) 50%, var(--color-bg-elevated) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +/* Empty state floating icon */ +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +/* Button interactive feedback */ +.btn-primary { + transition: transform 150ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 150ms ease; +} +.btn-primary:hover { + transform: scale(1.02); +} +.btn-primary:active { + transform: scale(0.97); + transition-duration: 50ms; +} + +.btn-icon:active { + transform: scale(0.85); + transition: transform 100ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.btn-icon-delete:active { + transform: scale(0.85) rotate(-10deg); + transition: transform 100ms cubic-bezier(0.22, 1, 0.36, 1); +} + +/* Card hover lift */ +.card-hover { + transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 200ms ease; +} +.card-hover:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} +.card-hover:active { + transform: translateY(0); +} + +/* Progress bar animate-in */ +.progress-bar { + transition: width 600ms cubic-bezier(0.22, 1, 0.36, 1); +} + +/* Timer start pulse */ +@keyframes timer-pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.03); } + 100% { transform: scale(1); } +} + +.animate-timer-pulse { + animation: timer-pulse 300ms cubic-bezier(0.22, 1, 0.36, 1); +} + +/* Timer stop glow */ +@keyframes timer-glow { + 0% { text-shadow: 0 0 0 transparent; } + 30% { text-shadow: 0 0 12px var(--color-accent-muted); } + 100% { text-shadow: 0 0 0 transparent; } +} + +.animate-timer-glow { + animation: timer-glow 600ms ease-out; +} + +/* Respect reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +}