mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-13 18:00:35 +08:00
docs: fix motion skill examples
Fix copied example issues from the adopted #1780 motion skills: live reduced-motion config, tokenized distances/easing/springs, valid shimmer skeleton JSX, and visibility cleanup.
This commit is contained in:
parent
f219a90f20
commit
cb2a70ce72
@ -278,7 +278,7 @@ import { motionTokens } from "@/lib/motion-tokens"
|
||||
d="M 0 100 Q 50 0 100 100"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{ duration: motionTokens.duration.slow, ease: "easeInOut" }}
|
||||
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
|
||||
/>
|
||||
```
|
||||
|
||||
@ -336,12 +336,13 @@ const { ref, style } = useScrollReveal()
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useMotionValue, useSpring } from "motion/react"
|
||||
import { springs } from "@/lib/motion-tokens"
|
||||
|
||||
export function CursorFollower() {
|
||||
const x = useMotionValue(-100)
|
||||
const y = useMotionValue(-100)
|
||||
const sx = useSpring(x, { stiffness: 120, damping: 16 })
|
||||
const sy = useSpring(y, { stiffness: 120, damping: 16 })
|
||||
const sx = useSpring(x, springs.gentle)
|
||||
const sy = useSpring(y, springs.gentle)
|
||||
|
||||
useEffect(() => {
|
||||
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
|
||||
@ -363,29 +364,44 @@ export function CursorFollower() {
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useAnimation } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function ShimmerSkeleton({ className }: { className?: string }) {
|
||||
export function ShimmerSkeleton({ className = "" }: { className?: string }) {
|
||||
const controls = useAnimation()
|
||||
|
||||
useEffect(() => {
|
||||
const run = () =>
|
||||
controls.start({ x: ["-100%", "100%"], transition: { repeat: Infinity, duration: 1.2, ease: "linear" } })
|
||||
const onVis = () => (document.visibilityState === "hidden" ? controls.stop() : run())
|
||||
run()
|
||||
document.addEventListener("visibilitychange", onVis)
|
||||
return () => document.removeEventListener("visibilitychange", onVis)
|
||||
const play = () =>
|
||||
controls.start({
|
||||
x: ["-100%", "100%"],
|
||||
transition: {
|
||||
repeat: Infinity,
|
||||
duration: motionTokens.duration.crawl,
|
||||
ease: motionTokens.easing.linear,
|
||||
},
|
||||
})
|
||||
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === "hidden") controls.stop()
|
||||
else void play()
|
||||
}
|
||||
|
||||
void play()
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
return () => {
|
||||
controls.stop()
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
}, [controls])
|
||||
return (
|
||||
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ repeat: Infinity, duration: 1.2, ease: "linear" }}
|
||||
|
||||
return (
|
||||
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
|
||||
initial={{ x: "-100%" }}
|
||||
animate={controls}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -443,8 +459,9 @@ export function LoadingButton({
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useAnimation } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function PulseDot() {
|
||||
const controls = useAnimation()
|
||||
@ -454,19 +471,23 @@ export function PulseDot() {
|
||||
controls.start({
|
||||
scale: [1, 1.4, 1],
|
||||
opacity: [1, 0.6, 1],
|
||||
transition: { repeat: Infinity, duration: 1.8 },
|
||||
transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
|
||||
})
|
||||
|
||||
// Rule 2: pause when tab is hidden
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === "hidden") controls.stop()
|
||||
else pulse()
|
||||
else void pulse()
|
||||
}
|
||||
|
||||
pulse()
|
||||
void pulse()
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
return () => document.removeEventListener("visibilitychange", handleVisibility) // Rule 7
|
||||
}, [])
|
||||
// Rule 7: stop controls and remove listeners on unmount.
|
||||
return () => {
|
||||
controls.stop()
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
}, [controls])
|
||||
|
||||
return <motion.span className="w-2 h-2 rounded-full bg-green-400" animate={controls} />
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ This skill produces:
|
||||
|
||||
- A shared `motionTokens` object (duration, easing, distance, scale)
|
||||
- A shared `springs` preset map (5 named configs)
|
||||
- A `shouldAnimate` boolean gate used by all components
|
||||
- A `shouldAnimate()` gate used by all components
|
||||
- Accessibility-compliant animation defaults via `useReducedMotion`
|
||||
- SSR-safe initial states with zero hydration warnings
|
||||
|
||||
@ -79,7 +79,7 @@ These are non-negotiable. They apply to every component in the system.
|
||||
|
||||
### When to disable animation entirely
|
||||
|
||||
Disable (set `shouldAnimate = false`) when:
|
||||
Disable (make `shouldAnimate()` return `false`) when:
|
||||
|
||||
- `prefersReduced` is `true`
|
||||
- `isLowEnd` is `true` and the animation is non-essential
|
||||
@ -134,20 +134,28 @@ export const springs = {
|
||||
```ts
|
||||
// lib/motion-config.ts
|
||||
export const motionConfig = {
|
||||
isLowEnd:
|
||||
typeof navigator !== "undefined" &&
|
||||
navigator.hardwareConcurrency <= 4,
|
||||
|
||||
prefersReduced:
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
||||
|
||||
get shouldAnimate() {
|
||||
return !this.prefersReduced
|
||||
isLowEnd() {
|
||||
return (
|
||||
typeof navigator !== "undefined" &&
|
||||
navigator.hardwareConcurrency <= 4
|
||||
)
|
||||
},
|
||||
|
||||
get duration() {
|
||||
return this.isLowEnd || this.prefersReduced
|
||||
prefersReduced() {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
)
|
||||
},
|
||||
|
||||
shouldAnimate({ essential = false } = {}) {
|
||||
if (this.prefersReduced()) return false
|
||||
if (!essential && this.isLowEnd()) return false
|
||||
return true
|
||||
},
|
||||
|
||||
duration() {
|
||||
return this.isLowEnd() || this.prefersReduced()
|
||||
? motionTokens.duration.instant
|
||||
: motionTokens.duration.normal
|
||||
},
|
||||
@ -240,7 +248,7 @@ export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
|
||||
const safeMotion = useSafeMotion(motionTokens.distance.md)
|
||||
|
||||
// Device gate — skip animation on low-end hardware
|
||||
if (!motionConfig.shouldAnimate || !mounted) {
|
||||
if (!motionConfig.shouldAnimate() || !mounted) {
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@ const item = {
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { springs } from "@/lib/motion-tokens"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
// Wrap at the call site:
|
||||
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
|
||||
@ -164,9 +164,17 @@ export function Modal({ onClose }: { onClose: () => void }) {
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 8 }}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: motionTokens.scale.press,
|
||||
y: motionTokens.distance.sm,
|
||||
}}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: motionTokens.scale.press,
|
||||
y: motionTokens.distance.sm,
|
||||
}}
|
||||
transition={springs.gentle}
|
||||
/>
|
||||
</>
|
||||
@ -179,16 +187,24 @@ export function Modal({ onClose }: { onClose: () => void }) {
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { springs } from "@/lib/motion-tokens"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
<AnimatePresence mode="sync">
|
||||
{toasts.map((t) => (
|
||||
<motion.div
|
||||
key={t.id}
|
||||
layout
|
||||
initial={{ opacity: 0, x: 48, scale: 0.9 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 48, scale: 0.9 }}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: motionTokens.distance.xl,
|
||||
scale: motionTokens.scale.subtle,
|
||||
}}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: motionTokens.distance.xl,
|
||||
scale: motionTokens.scale.subtle,
|
||||
}}
|
||||
transition={springs.snappy}
|
||||
/>
|
||||
))}
|
||||
@ -317,7 +333,7 @@ export function ExpandingCard({ title, body }: { title: string; body: string })
|
||||
initial={false}
|
||||
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
|
||||
style={{ transformOrigin: "top", overflow: "hidden" }}
|
||||
transition={{
|
||||
transition={{
|
||||
duration: motionTokens.duration.normal,
|
||||
ease: motionTokens.easing.smooth,
|
||||
}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user