From 6b7dcc7317a55aadd7df16f1fd32446a7cb51288 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 11:07:57 +0200 Subject: [PATCH] docs: add motion system design for animations and micro-interactions --- docs/plans/2026-02-18-motion-system-design.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/plans/2026-02-18-motion-system-design.md diff --git a/docs/plans/2026-02-18-motion-system-design.md b/docs/plans/2026-02-18-motion-system-design.md new file mode 100644 index 0000000..4ace627 --- /dev/null +++ b/docs/plans/2026-02-18-motion-system-design.md @@ -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 `` and `` 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 `` in `App.vue` with `` + ``: + +- **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 `` 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 `
` 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 ``: +- `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 `` 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 `` +- `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`