docs: add motion system design for animations and micro-interactions
This commit is contained in:
192
docs/plans/2026-02-18-motion-system-design.md
Normal file
192
docs/plans/2026-02-18-motion-system-design.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# ZeroClock Motion System Design
|
||||
|
||||
## Personality
|
||||
|
||||
Fluid & organic: spring-based easing with slight overshoot, 200-350ms durations for primary animations, 100-150ms for micro-interactions. The app should feel alive and responsive without being distracting.
|
||||
|
||||
## Technology
|
||||
|
||||
**@vueuse/motion** for declarative spring-physics animations via `v-motion` directives and `useMotion()` composable. Vue's built-in `<Transition>` and `<TransitionGroup>` for enter/leave orchestration. CSS keyframes for ambient/looping animations (pulse, shimmer, float).
|
||||
|
||||
## Spring Presets
|
||||
|
||||
Define reusable spring configs in a `src/utils/motion.ts` module:
|
||||
|
||||
- **snappy**: `{ damping: 20, stiffness: 300 }` — buttons, toggles, small elements
|
||||
- **smooth**: `{ damping: 15, stiffness: 200 }` — page transitions, modals, cards
|
||||
- **popIn**: `{ damping: 12, stiffness: 400 }` — tag chips, badges, notifications
|
||||
|
||||
## 1. Page Transitions
|
||||
|
||||
Wrap `<router-view>` in `App.vue` with `<router-view v-slot="{ Component }">` + `<Transition>`:
|
||||
|
||||
- **Leave**: opacity 1 -> 0, translateY 0 -> -8px, 150ms ease-out
|
||||
- **Enter**: opacity 0 -> 1, translateY 8px -> 0, spring `smooth` preset
|
||||
- **Mode**: `out-in` to prevent layout overlap in the flex container
|
||||
|
||||
CSS classes: `.page-enter-active`, `.page-leave-active`, `.page-enter-from`, `.page-leave-to`
|
||||
|
||||
## 2. List Animations
|
||||
|
||||
### Entry rows, project cards, client cards
|
||||
|
||||
Use `<TransitionGroup>` with staggered enter:
|
||||
|
||||
- **Enter**: opacity 0 -> 1, translateY 12px -> 0, spring `smooth`, stagger 30ms per item (via `transition-delay` computed from index)
|
||||
- **Leave**: opacity 1 -> 0, translateX 0 -> -20px, 150ms ease-in
|
||||
- **Move**: `transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1)` for reorder
|
||||
|
||||
CSS classes: `.list-enter-active`, `.list-leave-active`, `.list-move`, `.list-enter-from`, `.list-leave-to`
|
||||
|
||||
### Tag chips
|
||||
|
||||
- **Enter**: scale 0.8 -> 1, opacity 0 -> 1, spring `popIn`
|
||||
- **Leave**: scale 1 -> 0.8, opacity 1 -> 0, 100ms
|
||||
|
||||
### Favorites strip pills
|
||||
|
||||
- **Enter**: translateX -10px -> 0, opacity 0 -> 1, stagger 50ms
|
||||
- **Leave**: scale 1 -> 0, opacity 0, 100ms
|
||||
|
||||
### Recent entries in Timer
|
||||
|
||||
- **Enter**: opacity 0 -> 1, translateX -8px -> 0, stagger 40ms
|
||||
|
||||
## 3. Button & Interactive Feedback
|
||||
|
||||
### Primary buttons (Start/Stop, Generate, Save, Import)
|
||||
|
||||
- `:hover` — `scale(1.02)`, subtle shadow lift, spring `snappy`
|
||||
- `:active` — `scale(0.97)`, shadow compress, instant (no delay)
|
||||
- Release — spring back to `scale(1)` via `snappy` preset
|
||||
|
||||
### Icon buttons (edit, delete, copy, star, repeat)
|
||||
|
||||
- `:active` — `scale(0.85)`, spring back
|
||||
- Delete icons: `rotate(-10deg)` on active for a "shake" feel
|
||||
|
||||
### Nav rail active indicator
|
||||
|
||||
Currently the left border appears instantly. Change to:
|
||||
- Slide the 2px accent bar vertically to match the new active item position
|
||||
- Use a dedicated `<div>` with `v-motion` and absolute positioning keyed to `currentPath`
|
||||
- Spring `smooth` preset
|
||||
|
||||
### Toggle switches
|
||||
|
||||
- Add `cubic-bezier(0.34, 1.56, 0.64, 1)` (overshoot) to the thumb's `transition-transform`
|
||||
- Duration: 200ms (up from 150ms)
|
||||
|
||||
### Project/client cards
|
||||
|
||||
- `:hover` — `translateY(-1px)`, shadow elevation increase, 200ms
|
||||
- `:active` — `translateY(0)`, shadow compress
|
||||
|
||||
### Accent color picker dots in Settings
|
||||
|
||||
- Selected dot: `scale(1.15)` with spring
|
||||
- Hover: `scale(1.1)`
|
||||
|
||||
## 4. Loading & Empty States
|
||||
|
||||
### Skeleton shimmer
|
||||
|
||||
New CSS keyframe `shimmer`: a gradient highlight sweeping left-to-right on placeholder blocks. Apply via `.skeleton` utility class.
|
||||
|
||||
```
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
```
|
||||
|
||||
### Content fade-in
|
||||
|
||||
When data loads (entries, projects, reports), wrap the content area in a `<Transition>`:
|
||||
- `opacity: 0` -> `1`, `translateY: 4px` -> `0`, spring `smooth`
|
||||
- Show skeleton while `loading` ref is true, transition to real content
|
||||
|
||||
### Empty state icons
|
||||
|
||||
Add gentle vertical float animation to the empty state icons (Timer, BarChart3):
|
||||
|
||||
```
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
```
|
||||
|
||||
Duration: 3s, ease-in-out, infinite.
|
||||
|
||||
## 5. Modal & Dropdown Polish
|
||||
|
||||
### Modals
|
||||
|
||||
Replace `animate-modal-enter` keyframe with spring-based:
|
||||
|
||||
- **Backdrop enter**: opacity 0 -> 1, 200ms ease-out
|
||||
- **Backdrop leave**: opacity 1 -> 0, 150ms ease-in
|
||||
- **Panel enter**: scale 0.95 -> 1, opacity 0 -> 1, spring `smooth`
|
||||
- **Panel leave**: scale 1 -> 0.97, opacity 1 -> 0, 150ms ease-in
|
||||
|
||||
Use Vue `<Transition>` on the modal's `v-if` to get proper leave animations (currently missing — modals just vanish).
|
||||
|
||||
### Dropdowns (AppSelect, AppTagInput, AppDatePicker)
|
||||
|
||||
Replace `animate-dropdown-enter` with spring:
|
||||
- **Enter**: scale 0.95 -> 1, opacity 0 -> 1, translateY -4px -> 0, spring `snappy`
|
||||
- **Leave**: opacity 1 -> 0, 100ms (fast close)
|
||||
|
||||
### Toast notifications
|
||||
|
||||
Keep existing enter/exit keyframes but add slight horizontal slide:
|
||||
- **Enter**: translateY -20px -> 0, translateX 10px -> 0 (slide from top-right)
|
||||
- **Exit**: opacity fade + translateY -10px
|
||||
|
||||
## 6. Timer-Specific Animations
|
||||
|
||||
### Timer start
|
||||
|
||||
When the timer transitions from STOPPED to RUNNING:
|
||||
- The time display pulses once (scale 1 -> 1.03 -> 1) via spring
|
||||
- The Start button morphs to Stop (color transition already exists, add scale pulse)
|
||||
|
||||
### Timer stop
|
||||
|
||||
When the timer stops:
|
||||
- Brief "completed" flash — the time display gets a subtle glow/highlight that fades
|
||||
|
||||
### Progress bars (goals, budgets)
|
||||
|
||||
- Animate width from 0 to target value on mount, 600ms with spring `smooth`
|
||||
- Use CSS `transition: width 600ms cubic-bezier(0.22, 1, 0.36, 1)`
|
||||
|
||||
## Files to Modify
|
||||
|
||||
- `package.json` — add `@vueuse/motion`
|
||||
- `src/main.ts` — register `MotionPlugin`
|
||||
- `src/utils/motion.ts` — new, spring preset definitions
|
||||
- `src/styles/main.css` — new keyframes (shimmer, float), update existing keyframes, add transition utility classes
|
||||
- `src/App.vue` — page transition wrapper on `<router-view>`
|
||||
- `src/components/NavRail.vue` — animated active indicator
|
||||
- `src/components/AppSelect.vue` — dropdown leave animation
|
||||
- `src/components/AppTagInput.vue` — tag chip enter/leave transitions
|
||||
- `src/components/ToastNotification.vue` — enhanced enter/exit
|
||||
- `src/views/Timer.vue` — timer start/stop animations, list transition on recent entries
|
||||
- `src/views/Entries.vue` — TransitionGroup on entry rows
|
||||
- `src/views/Projects.vue` — TransitionGroup on project cards, modal transitions
|
||||
- `src/views/Clients.vue` — TransitionGroup on client cards, modal transitions
|
||||
- `src/views/Dashboard.vue` — content fade-in, progress bar animation
|
||||
- `src/views/Reports.vue` — content fade-in on tab switch
|
||||
- `src/views/Settings.vue` — modal leave transition
|
||||
- `src/views/CalendarView.vue` — entry block fade-in on week change
|
||||
- `src/views/TimesheetView.vue` — row fade-in on data load
|
||||
|
||||
## Verification
|
||||
|
||||
1. `npm run build` passes with no errors
|
||||
2. Page transitions feel smooth, no layout flashing
|
||||
3. List add/remove animations don't cause scroll jumps
|
||||
4. Modals have proper enter AND leave animations
|
||||
5. No animation on `prefers-reduced-motion: reduce`
|
||||
Reference in New Issue
Block a user