mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-15 13:23:13 +08:00
feat: add motion system skills
Adopts the motion skill content from PR #1780 and syncs the public catalog counts for the current main surface. Co-authored-by: Jeff <peacelord1309@gmail.com>
This commit is contained in:
committed by
Affaan Mustafa
parent
22aabf7d4f
commit
f219a90f20
291
skills/motion-foundations/SKILL.md
Normal file
291
skills/motion-foundations/SKILL.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
name: motion-foundations
|
||||
description: Motion tokens, spring presets, performance rules, device adaptation, accessibility enforcement, and SSR safety for React / Next.js using motion/react. Foundation layer — all other motion skills depend on this.
|
||||
version: 1.0
|
||||
tags: [motion, animation, performance, accessibility]
|
||||
category: frontend
|
||||
author: jeff
|
||||
---
|
||||
|
||||
# Motion Foundations
|
||||
|
||||
The base layer of the motion system. Defines every value, constraint, and
|
||||
rule that downstream skills (`motion-patterns`, `motion-advanced`) inherit.
|
||||
Load this skill before any animation work begins.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Starting any animated component from scratch
|
||||
- Setting up tokens, spring presets, or easing values
|
||||
- Implementing `prefers-reduced-motion` support
|
||||
- Debugging hydration mismatches from animation initial states
|
||||
- Evaluating whether an animation should exist at all
|
||||
|
||||
## Outputs
|
||||
|
||||
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
|
||||
- Accessibility-compliant animation defaults via `useReducedMotion`
|
||||
- SSR-safe initial states with zero hydration warnings
|
||||
|
||||
## Principles
|
||||
|
||||
Motion must do at least one of the following or it must be removed:
|
||||
|
||||
- Guide attention
|
||||
- Communicate state
|
||||
- Preserve spatial continuity
|
||||
|
||||
Responsiveness always outranks smoothness. A 60 fps animation that causes
|
||||
input delay is worse than no animation.
|
||||
|
||||
## Rules
|
||||
|
||||
These are non-negotiable. They apply to every component in the system.
|
||||
|
||||
1. **Use `motion/react` only.** Never import from `framer-motion`. Never mix the two in the same tree.
|
||||
2. **`initial` must match server output.** If the server renders `opacity: 1`, the `initial` prop must also be `opacity: 1`. No exceptions.
|
||||
3. **Reduced motion overrides everything.** When `useReducedMotion()` returns `true` or `prefersReduced` is `true`, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
|
||||
4. **Never animate layout properties.** `width`, `height`, `top`, `left`, `margin`, `padding` are banned from `animate`. Use `transform` and `opacity` only.
|
||||
5. **All token values come from `motionTokens`.** Hardcoded durations and easings in component files are forbidden.
|
||||
6. **All spring configs come from the `springs` map.** Inline `stiffness`/`damping` values are forbidden.
|
||||
7. **`"use client"` is required** on every file that imports from `motion/react`.
|
||||
8. **Never read `window` or `navigator` at module level.** Always guard with `typeof window !== "undefined"`.
|
||||
|
||||
## Decision Guidance
|
||||
|
||||
### Choosing a duration
|
||||
|
||||
| Token | Use when |
|
||||
| --------- | -------------------------------------------- |
|
||||
| `instant` | Tooltip show/hide, focus ring, badge update |
|
||||
| `fast` | Button feedback, icon swap, chip toggle |
|
||||
| `normal` | Modal open, card expand, page element enter |
|
||||
| `slow` | Hero entrance, full-page transition |
|
||||
| `crawl` | Deliberate storytelling; use sparingly |
|
||||
|
||||
### Choosing a spring
|
||||
|
||||
| Preset | Use when |
|
||||
| --------- | ------------------------------------------ |
|
||||
| `snappy` | Default UI — buttons, chips, nav items |
|
||||
| `gentle` | Cards, modals, panels landing softly |
|
||||
| `bouncy` | Playful moments — empty states, onboarding |
|
||||
| `instant` | Tooltips, popovers, dropdowns |
|
||||
| `release` | Drag release — natural physics feel |
|
||||
|
||||
### When to disable animation entirely
|
||||
|
||||
Disable (set `shouldAnimate = false`) when:
|
||||
|
||||
- `prefersReduced` is `true`
|
||||
- `isLowEnd` is `true` and the animation is non-essential
|
||||
- The element is off-screen and will never enter the viewport
|
||||
- The animation is purely decorative with no UX purpose
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Token system
|
||||
|
||||
```ts
|
||||
// lib/motion-tokens.ts
|
||||
export const motionTokens = {
|
||||
duration: {
|
||||
instant: 0.08,
|
||||
fast: 0.18,
|
||||
normal: 0.35,
|
||||
slow: 0.6,
|
||||
crawl: 1.0,
|
||||
},
|
||||
easing: {
|
||||
smooth: [0.22, 1, 0.36, 1],
|
||||
sharp: [0.4, 0, 0.2, 1],
|
||||
bounce: [0.34, 1.56, 0.64, 1],
|
||||
linear: [0, 0, 1, 1],
|
||||
},
|
||||
distance: {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 48,
|
||||
},
|
||||
scale: {
|
||||
subtle: 0.98,
|
||||
press: 0.95,
|
||||
pop: 1.04,
|
||||
},
|
||||
}
|
||||
|
||||
export const springs = {
|
||||
snappy: { type: "spring", stiffness: 300, damping: 30 },
|
||||
gentle: { type: "spring", stiffness: 120, damping: 14 },
|
||||
bouncy: { type: "spring", stiffness: 400, damping: 10 },
|
||||
instant: { type: "spring", stiffness: 600, damping: 35 },
|
||||
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime flags
|
||||
|
||||
```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
|
||||
},
|
||||
|
||||
get duration() {
|
||||
return this.isLowEnd || this.prefersReduced
|
||||
? motionTokens.duration.instant
|
||||
: motionTokens.duration.normal
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
**Priority order (highest to lowest):**
|
||||
|
||||
1. `prefers-reduced-motion: reduce` — disables all transforms, limits opacity transitions to ≤ 0.2s
|
||||
2. Low-end device detection — reduces duration, removes non-essential animations
|
||||
3. Design preference — everything else
|
||||
|
||||
Motion must degrade gracefully. It must never disappear abruptly in a way
|
||||
that causes layout shift or confuses orientation.
|
||||
|
||||
```tsx
|
||||
// hooks/use-reduced-motion.tsx
|
||||
"use client"
|
||||
import { useReducedMotion } from "motion/react"
|
||||
|
||||
export function useSafeMotion(fullY: number = 16) {
|
||||
const reduce = useReducedMotion()
|
||||
return {
|
||||
initial: { opacity: 0, y: reduce ? 0 : fullY },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: reduce ? 0 : -fullY },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* globals.css */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-safe-transition { transition: opacity 0.15s; }
|
||||
.motion-reduce-transform { transform: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Tailwind -->
|
||||
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
|
||||
```
|
||||
|
||||
### SSR / hydration safety
|
||||
|
||||
**Rule: `initial` must always match what the server renders.**
|
||||
|
||||
```tsx
|
||||
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
|
||||
|
||||
// CORRECT — use AnimatePresence or defer to client mount
|
||||
"use client"
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: mounted ? 0 : 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### End-to-end: tokens + springs + accessibility + SSR guard
|
||||
|
||||
```tsx
|
||||
// components/fade-in-card.tsx
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
import { useSafeMotion } from "@/hooks/use-reduced-motion"
|
||||
import { motionConfig } from "@/lib/motion-config"
|
||||
|
||||
interface FadeInCardProps {
|
||||
children: React.ReactNode
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
|
||||
// SSR guard — initial must match server output (opacity: 1)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
// Accessibility — disables transform when reduced motion is preferred
|
||||
const safeMotion = useSafeMotion(motionTokens.distance.md)
|
||||
|
||||
// Device gate — skip animation on low-end hardware
|
||||
if (!motionConfig.shouldAnimate || !mounted) {
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={safeMotion.initial}
|
||||
animate={safeMotion.animate}
|
||||
exit={safeMotion.exit}
|
||||
transition={{
|
||||
...springs.gentle,
|
||||
delay,
|
||||
}}
|
||||
whileHover={{ scale: motionTokens.scale.pop }}
|
||||
whileTap={{ scale: motionTokens.scale.press }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints / Non-Goals
|
||||
|
||||
This skill does **not** cover:
|
||||
|
||||
- UI component patterns (button, modal, stagger) → see `motion-patterns`
|
||||
- Drag, gestures, SVG, text animations, custom hooks → see `motion-advanced`
|
||||
- CSS-only animations or Tailwind `animate-*` classes without `motion/react`
|
||||
- Third-party animation libraries (GSAP, anime.js, etc.)
|
||||
- Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-pattern | Rule violated | Fix |
|
||||
| --------------------------------------- | ------- | ------------------------------- |
|
||||
| `import { motion } from "framer-motion"` | Rule 1 | Use `motion/react` |
|
||||
| `initial={{ opacity: 0 }}` on SSR component | Rule 2 | Add mount guard |
|
||||
| Skipping `useReducedMotion` check | Rule 3 | Use `useSafeMotion` hook |
|
||||
| `animate={{ width: "100%" }}` | Rule 4 | Use `scaleX` transform instead |
|
||||
| `transition={{ duration: 0.4 }}` inline | Rule 5 | Use `motionTokens.duration.normal` |
|
||||
| `{ stiffness: 300, damping: 30 }` inline | Rule 6 | Use `springs.snappy` |
|
||||
| Missing `"use client"` directive | Rule 7 | Add to top of file |
|
||||
| `navigator.hardwareConcurrency` at module level | Rule 8 | Wrap in `typeof navigator !== "undefined"` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **`motion-patterns`** — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
|
||||
- **`motion-advanced`** — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds `useAnimate` sequences and custom hooks on top of this foundation.
|
||||
Reference in New Issue
Block a user