mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +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"
|
d="M 0 100 Q 50 0 100 100"
|
||||||
initial={{ pathLength: 0, opacity: 0 }}
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
animate={{ pathLength: 1, opacity: 1 }}
|
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"
|
"use client"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { motion, useMotionValue, useSpring } from "motion/react"
|
import { motion, useMotionValue, useSpring } from "motion/react"
|
||||||
|
import { springs } from "@/lib/motion-tokens"
|
||||||
|
|
||||||
export function CursorFollower() {
|
export function CursorFollower() {
|
||||||
const x = useMotionValue(-100)
|
const x = useMotionValue(-100)
|
||||||
const y = useMotionValue(-100)
|
const y = useMotionValue(-100)
|
||||||
const sx = useSpring(x, { stiffness: 120, damping: 16 })
|
const sx = useSpring(x, springs.gentle)
|
||||||
const sy = useSpring(y, { stiffness: 120, damping: 16 })
|
const sy = useSpring(y, springs.gentle)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
|
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
|
||||||
@ -363,29 +364,44 @@ export function CursorFollower() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
"use client"
|
"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()
|
const controls = useAnimation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const run = () =>
|
const play = () =>
|
||||||
controls.start({ x: ["-100%", "100%"], transition: { repeat: Infinity, duration: 1.2, ease: "linear" } })
|
controls.start({
|
||||||
const onVis = () => (document.visibilityState === "hidden" ? controls.stop() : run())
|
x: ["-100%", "100%"],
|
||||||
run()
|
transition: {
|
||||||
document.addEventListener("visibilitychange", onVis)
|
repeat: Infinity,
|
||||||
return () => document.removeEventListener("visibilitychange", onVis)
|
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])
|
}, [controls])
|
||||||
return (
|
|
||||||
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
|
return (
|
||||||
<motion.div
|
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
|
<motion.div
|
||||||
animate={{ x: ["-100%", "100%"] }}
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
|
||||||
transition={{ repeat: Infinity, duration: 1.2, ease: "linear" }}
|
initial={{ x: "-100%" }}
|
||||||
animate={controls}
|
animate={controls}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -443,8 +459,9 @@ export function LoadingButton({
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
"use client"
|
"use client"
|
||||||
import { useEffect, useRef } from "react"
|
import { useEffect } from "react"
|
||||||
import { motion, useAnimation } from "motion/react"
|
import { motion, useAnimation } from "motion/react"
|
||||||
|
import { motionTokens } from "@/lib/motion-tokens"
|
||||||
|
|
||||||
export function PulseDot() {
|
export function PulseDot() {
|
||||||
const controls = useAnimation()
|
const controls = useAnimation()
|
||||||
@ -454,19 +471,23 @@ export function PulseDot() {
|
|||||||
controls.start({
|
controls.start({
|
||||||
scale: [1, 1.4, 1],
|
scale: [1, 1.4, 1],
|
||||||
opacity: [1, 0.6, 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
|
// Rule 2: pause when tab is hidden
|
||||||
const handleVisibility = () => {
|
const handleVisibility = () => {
|
||||||
if (document.visibilityState === "hidden") controls.stop()
|
if (document.visibilityState === "hidden") controls.stop()
|
||||||
else pulse()
|
else void pulse()
|
||||||
}
|
}
|
||||||
|
|
||||||
pulse()
|
void pulse()
|
||||||
document.addEventListener("visibilitychange", handleVisibility)
|
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} />
|
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 `motionTokens` object (duration, easing, distance, scale)
|
||||||
- A shared `springs` preset map (5 named configs)
|
- 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`
|
- Accessibility-compliant animation defaults via `useReducedMotion`
|
||||||
- SSR-safe initial states with zero hydration warnings
|
- 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
|
### When to disable animation entirely
|
||||||
|
|
||||||
Disable (set `shouldAnimate = false`) when:
|
Disable (make `shouldAnimate()` return `false`) when:
|
||||||
|
|
||||||
- `prefersReduced` is `true`
|
- `prefersReduced` is `true`
|
||||||
- `isLowEnd` is `true` and the animation is non-essential
|
- `isLowEnd` is `true` and the animation is non-essential
|
||||||
@ -134,20 +134,28 @@ export const springs = {
|
|||||||
```ts
|
```ts
|
||||||
// lib/motion-config.ts
|
// lib/motion-config.ts
|
||||||
export const motionConfig = {
|
export const motionConfig = {
|
||||||
isLowEnd:
|
isLowEnd() {
|
||||||
typeof navigator !== "undefined" &&
|
return (
|
||||||
navigator.hardwareConcurrency <= 4,
|
typeof navigator !== "undefined" &&
|
||||||
|
navigator.hardwareConcurrency <= 4
|
||||||
prefersReduced:
|
)
|
||||||
typeof window !== "undefined" &&
|
|
||||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
||||||
|
|
||||||
get shouldAnimate() {
|
|
||||||
return !this.prefersReduced
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get duration() {
|
prefersReduced() {
|
||||||
return this.isLowEnd || this.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.instant
|
||||||
: motionTokens.duration.normal
|
: motionTokens.duration.normal
|
||||||
},
|
},
|
||||||
@ -240,7 +248,7 @@ export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
|
|||||||
const safeMotion = useSafeMotion(motionTokens.distance.md)
|
const safeMotion = useSafeMotion(motionTokens.distance.md)
|
||||||
|
|
||||||
// Device gate — skip animation on low-end hardware
|
// Device gate — skip animation on low-end hardware
|
||||||
if (!motionConfig.shouldAnimate || !mounted) {
|
if (!motionConfig.shouldAnimate() || !mounted) {
|
||||||
return <div>{children}</div>
|
return <div>{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -141,7 +141,7 @@ const item = {
|
|||||||
```tsx
|
```tsx
|
||||||
"use client"
|
"use client"
|
||||||
import { motion, AnimatePresence } from "motion/react"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
import { springs } from "@/lib/motion-tokens"
|
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||||
|
|
||||||
// Wrap at the call site:
|
// Wrap at the call site:
|
||||||
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
|
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
|
||||||
@ -164,9 +164,17 @@ export function Modal({ onClose }: { onClose: () => void }) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
|
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 }}
|
initial={{
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
opacity: 0,
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 8 }}
|
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}
|
transition={springs.gentle}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -179,16 +187,24 @@ export function Modal({ onClose }: { onClose: () => void }) {
|
|||||||
```tsx
|
```tsx
|
||||||
"use client"
|
"use client"
|
||||||
import { motion, AnimatePresence } from "motion/react"
|
import { motion, AnimatePresence } from "motion/react"
|
||||||
import { springs } from "@/lib/motion-tokens"
|
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||||
|
|
||||||
<AnimatePresence mode="sync">
|
<AnimatePresence mode="sync">
|
||||||
{toasts.map((t) => (
|
{toasts.map((t) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={t.id}
|
key={t.id}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, x: 48, scale: 0.9 }}
|
initial={{
|
||||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
opacity: 0,
|
||||||
exit={{ opacity: 0, x: 48, scale: 0.9 }}
|
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}
|
transition={springs.snappy}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -317,7 +333,7 @@ export function ExpandingCard({ title, body }: { title: string; body: string })
|
|||||||
initial={false}
|
initial={false}
|
||||||
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
|
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
|
||||||
style={{ transformOrigin: "top", overflow: "hidden" }}
|
style={{ transformOrigin: "top", overflow: "hidden" }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: motionTokens.duration.normal,
|
duration: motionTokens.duration.normal,
|
||||||
ease: motionTokens.easing.smooth,
|
ease: motionTokens.easing.smooth,
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user