Files
zeroclock/docs/plans/2026-02-18-motion-system-design.md

6.9 KiB

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)

  • :hoverscale(1.02), subtle shadow lift, spring snappy
  • :activescale(0.97), shadow compress, instant (no delay)
  • Release — spring back to scale(1) via snappy preset

Icon buttons (edit, delete, copy, star, repeat)

  • :activescale(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

  • :hovertranslateY(-1px), shadow elevation increase, 200ms
  • :activetranslateY(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