Animate quantity steppers and make the nav bar sticky

- Add an AnimatedNumber component that slides a quantity stepper's
  count horizontally — left on increment, right on decrement — and
  wire it into both the menu and basket steppers.
- Make the top nav bar position: sticky, with an explicit background
  so scrolled content doesn't show through it.
- Reserve the scrollbar's gutter on every page (scrollable or not) so
  the sticky header doesn't shift width when navigating between short
  and tall pages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 12:30:56 +02:00
co-authored by Claude Sonnet 5
parent 79bd64ce8c
commit e98b0d86d2
6 changed files with 86 additions and 3 deletions
+58
View File
@@ -0,0 +1,58 @@
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import { useState } from 'react'
import styles from './AnimatedNumber.module.scss'
type AnimatedNumberProps = {
value: number
className?: string
}
const variants = {
initial: (direction: number) => ({ x: direction * 12, opacity: 0 }),
animate: { x: 0, opacity: 1 },
exit: (direction: number) => ({ x: direction * -12, opacity: 0 }),
}
// A quantity stepper's count slides leftward when incrementing (the new
// digit enters from the right, the old one exits to the left) and
// rightward when decrementing.
//
// `direction` is derived from a prop change during render (the React-docs
// "adjusting state when a prop changes" pattern) rather than mutating a ref
// in the render body — mutating a ref here would already reflect the new
// value by the time `exit`'s "custom" is resolved for the outgoing digit,
// so every transition — increment or decrement — read as "forward".
export const AnimatedNumber = ({ value, className }: AnimatedNumberProps) => {
const reduceMotion = useReducedMotion()
const [previousValue, setPreviousValue] = useState(value)
const [direction, setDirection] = useState(1)
if (value !== previousValue) {
setDirection(value > previousValue ? 1 : -1)
setPreviousValue(value)
}
if (reduceMotion) {
return <span className={className}>{value}</span>
}
return (
<span className={`${styles.wrapper} ${className ?? ''}`}>
<AnimatePresence mode="popLayout" initial={false} custom={direction}>
<motion.span
key={value}
custom={direction}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.15, ease: 'easeOut' }}
className={styles.digit}
>
{value}
</motion.span>
</AnimatePresence>
</span>
)
}