refactor(flip-card): composable Front/Back API with motion-safe code#511
Conversation
…forms Replace fixed image/title props with FlipCard.Front and FlipCard.Back. Consumers own face markup. Flip uses transform-only transitions with reduced-motion guards. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThree composable API refactors land together: ChangesCard Composition Refactor
SplitReveal Modularization
Build and CI Infrastructure
Sequence Diagram(s)sequenceDiagram
participant App
participant SplitRevealRoot
participant SplitRevealTask
participant SplitRevealImages
participant executeTask
participant SplitRevealOverlay
App->>SplitRevealRoot: render with lockScroll, onComplete
SplitRevealRoot->>SplitRevealRoot: provide SplitRevealContext + SplitRevealInternalContext
SplitRevealImages->>SplitRevealTask: register run callback via SplitRevealTask
SplitRevealTask->>SplitRevealRoot: registerTask(id, definition)
SplitRevealRoot->>executeTask: bootstrap — run all tasks in parallel
executeTask->>SplitRevealRoot: report({ loaded, total }) per image
SplitRevealRoot->>SplitRevealRoot: phase loading → fade-ui → reveal → done
SplitRevealOverlay->>SplitRevealRoot: reads phase, zIndex, timing via useSplitReveal()
SplitRevealRoot->>App: onComplete()
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying animata with
|
| Latest commit: |
32f5c13
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ebaf3458.animata.pages.dev |
| Branch Preview URL: | https://refactor-flip-card-composabl.animata.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@animata/card/flip-card.tsx`:
- Around line 13-21: The hover rotation entries in ROTATION_CLASS (the hover
strings for keys x and y) cancel the flip under prefers-reduced-motion causing
FlipCardBack to remain pre-rotated and inaccessible; remove the motion-reduce
override tokens ("motion-reduce:group-hover/card:rotate-x-0" and
"motion-reduce:group-hover/card:rotate-y-0") from the hover values so the hover
still applies instantly for reduced-motion users (the existing
motion-reduce:transition-none handling at the transition site can remain as-is);
keep the back rotations ("rotate-x-180" / "rotate-y-180") unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ae1301a5-1e28-4a41-8f14-241aa590eb0a
📒 Files selected for processing (6)
animata/card/flip-card.stories.tsxanimata/card/flip-card.tsxanimata/card/swap-card.tsxcontent/docs/card/flip-card.mdxcontent/docs/card/index.mdxcontent/docs/changelog/2026-06.mdx
Replace list prop and inline 3D flip with FlippingCards.Item.Front/Back. Storybook demo and docs updated; accent helper exported for back faces. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@animata/list/flipping-cards.stories.tsx`:
- Around line 63-67: The story hardcodes light-only colors in
FlippingCards.Item.Front and its children; make these classes theme-responsive
by replacing static color classes with light/dark-aware classes. Update
FlippingCards.Item.Front’s className from "flex bg-white" to include a dark
variant (e.g. "flex bg-white dark:bg-slate-900" or your design token like "flex
bg-surface dark:bg-surface-dark"), change the inner div’s border/text classes
from "border border-black/15 ... text-sm" to use dual-mode classes (e.g. "border
border-black/15 dark:border-white/15 text-black dark:text-white px-3 py-4
text-sm"), and update the span classes that use "text-black" and "border-black"
to corresponding "text-black dark:text-white" and "border-black/15
dark:border-white/15" (or your token equivalents) so the face adapts in dark
mode. Ensure all color uses within FlippingCards.Item.Front, the inner
container, and both spans are converted to light/dark variants or theme tokens.
In `@animata/list/flipping-cards.tsx`:
- Around line 31-33: Replace the arbitrary HSL hue math in
getFlippingCardsAccent with the theme accent token so the component uses the
shared accent styling; specifically, change the function
getFlippingCardsAccent(index: number) to return the theme value like
"hsl(var(--accent))" (or "hsl(var(--accent) / <alpha>)" if you need opacity)
instead of computing `(index * 47) % 360`.
In `@content/docs/list/flipping-cards.mdx`:
- Line 21: The docs note incorrectly lists Marquee as a required install for the
flipping-cards example; update content/docs/list/flipping-cards.mdx to remove
the sentence that instructs installing Marquee and instead mention only the
required Flip Card, and confirm the example file animata/list/flipping-cards.tsx
(which does not import or use Marquee) needs no code changes—just update the
text to say "This uses [Flip Card](/docs/card/flip-card) for the hover flip" and
remove the Marquee install reference.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9acbc95c-a295-48ea-8d5a-2f34ea4e7f5f
📒 Files selected for processing (5)
animata/list/flipping-cards.stories.tsxanimata/list/flipping-cards.tsxcontent/docs/changelog/2026-06.mdxcontent/docs/list/flipping-cards.mdxcontent/docs/list/index.mdx
Opt-in Images and Task primitives, external ready/progress control, and tree-shakeable modules. Registry bundler follows co-located imports; photographer demo uses the default Progress UI. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (1)
animata/preloader/split-reveal/index.tsx (1)
3-3: ⚡ Quick winUse a co-located stylesheet for this TSX entrypoint.
index.tsximports../split-reveal.css; this breaks the co-location rule foranimata/**/*.{tsx,css}. Please move/rename to a co-located file (for example./index.css) and import it locally.As per coding guidelines,
animata/**/*.{tsx,css}: “Create co-located<name>.cssfiles for keyframes, pseudo-elements, and selectors that Tailwind cannot express, and import them from the corresponding TSX file.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@animata/preloader/split-reveal/index.tsx` at line 3, The index.tsx file imports a non-co-located stylesheet at ../split-reveal.css, which violates the co-location rule for animata components. Move the split-reveal.css file to the same directory as index.tsx and rename it to index.css, then update the import statement in index.tsx to reference the co-located file with a relative path like ./index.css instead of ../split-reveal.css.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@animata/preloader/split-reveal/execute-task.ts`:
- Around line 15-27: The abort handling in the generator execution loop has two
issues: first, iterator.next() is called at the start of each loop iteration
before checking signal.aborted, allowing one extra generator step to execute
even when aborted; second, when returning due to abort (when signal.aborted is
true), the code returns without calling iterator.return(), which skips generator
cleanup such as finally blocks. To fix this, move the abort check
(signal.aborted) to occur before calling iterator.next() in the while loop
condition or at the loop start, and when returning due to abort, call
iterator.return() first to ensure all cleanup code in the generator executes
properly.
In `@animata/preloader/split-reveal/overlay.tsx`:
- Around line 23-35: The overlay component spreads the consumer-provided props
after defining the required style, which allows a consumer-provided style prop
to override the critical overlay styles (zIndex, --split-reveal-duration,
--split-reveal-progress-fade). Move the style prop definition to come after the
{...props} spread so that the required internal styles take precedence and
cannot be overridden by consumer props, ensuring the overlay behavior is not
broken.
In `@animata/preloader/split-reveal/progress-count.tsx`:
- Around line 20-24: The inline styles in the progress-count component use
hex-suffix opacity notation (${foregroundColor}73 and ${foregroundColor}33)
which only works with hex color strings and breaks with hsl(), rgb(), or CSS
variable values. Replace these color assignments with a format that works
universally across all color types, such as using the rgba() function with a
calculated alpha value or applying opacity through a separate CSS property that
works regardless of the foregroundColor format.
In `@animata/preloader/split-reveal/root.tsx`:
- Around line 21-22: The backgroundColor and foregroundColor default parameters
in the SplitReveal component are hard-coded to light theme values (`#fff` and
`#000`), which breaks the component's appearance in dark mode. Replace these
hard-coded color defaults with theme-responsive tokens that automatically adapt
to the current theme context, ensuring the component works correctly for both
light and dark themes as required by the coding guidelines.
- Around line 121-124: The useEffect hook that manages task bootstrap is
including `controlledReady` and `controlledProgress` in its dependency list,
which causes it to re-run and abort in-flight executeTask calls whenever these
props change. Remove `controlledReady` and `controlledProgress` from the
dependency array of the useEffect that contains the task bootstrap logic. The
effect should initialize the task once at mount, and changing the `ready` flag
should only unblock the reveal without restarting the actual task work defined
in the SplitRevealTask.
- Around line 237-248: The Promise.all() chain for executing tasks only calls
the completion logic in the .then() handler, which means if any task rejects,
tasksDone never gets set to true, phase remains "loading", and the page stays
frozen in a deadlock state. Add error handling to the Promise chain by attaching
a .catch() or .finally() block that ensures the same completion logic (setting
tasksDone = true and calling maybeReveal(currentRun)) executes regardless of
whether the tasks succeed or fail. Make sure the abort signal check (currentRun
!== runId || abortController?.signal.aborted) is applied in both the success and
failure paths to prevent race conditions.
In `@animata/preloader/split-reveal/task.tsx`:
- Around line 25-36: The useEffect dependency array in the task registration is
incomplete. The effect creates a task definition using runRef.current and
generatorRef.current, but the dependency array only includes id and
registerTask. When the run or generator props change, their ref values update
but the effect does not re-run, causing the registered task to have stale
generator and run values. Add runRef and generatorRef (or their current values
if they are stable) to the dependency array so the task gets re-registered
whenever these values change.
In `@animata/preloader/split-reveal/use-prefers-reduced-motion.ts`:
- Around line 5-13: The subscribeReducedMotion function uses addEventListener
and removeEventListener on the MediaQueryList object, but these methods are not
supported in older browsers. Add a feature check to detect whether
addEventListener exists on the MediaQueryList object returned by
window.matchMedia. If it does not exist, fall back to using the deprecated
addListener method for adding the listener, and removeListener for removing it
in the cleanup function. This will ensure compatibility with older browser
versions while maintaining modern browser behavior.
In `@scripts/build-registry.js`:
- Around line 216-218: Relative paths with `../../` sequences can normalize
outside the project root and still be read and bundled into the registry JSON,
leaking unintended local files. After normalizing the path using
path.posix.normalize in the path resolution logic around line 216, add a
validation check to ensure the fully resolved absolute path (when joined with
ROOT) stays within the project root directory before returning it as a valid
base. The same constraint should be enforced at the bundling site around lines
256-259 to prevent any escaped paths from being included in the generated
registry output.
- Around line 267-268: The regex pattern in the `re` variable at lines 267-268
does not match TypeScript type re-exports (`export type { ... } from`). Update
the regex to include `type\s+` as an optional pattern in the export alternatives
so that both `export { ... } from` and `export type { ... } from` syntax are
matched and properly captured during bundling/classification.
---
Nitpick comments:
In `@animata/preloader/split-reveal/index.tsx`:
- Line 3: The index.tsx file imports a non-co-located stylesheet at
../split-reveal.css, which violates the co-location rule for animata components.
Move the split-reveal.css file to the same directory as index.tsx and rename it
to index.css, then update the import statement in index.tsx to reference the
co-located file with a relative path like ./index.css instead of
../split-reveal.css.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fbfaae16-71a4-4d95-88ef-4390cc4fb72d
⛔ Files ignored due to path filters (1)
app/demo/generated/demo-sources.tsis excluded by!**/generated/**
📒 Files selected for processing (24)
animata/preloader/split-reveal.stories.tsxanimata/preloader/split-reveal.tsxanimata/preloader/split-reveal/context.tsxanimata/preloader/split-reveal/execute-task.tsanimata/preloader/split-reveal/images.tsxanimata/preloader/split-reveal/index.tsxanimata/preloader/split-reveal/overlay.tsxanimata/preloader/split-reveal/preload-images.tsanimata/preloader/split-reveal/progress-count.tsxanimata/preloader/split-reveal/progress-slot.tsxanimata/preloader/split-reveal/progress-track.tsxanimata/preloader/split-reveal/progress.tsxanimata/preloader/split-reveal/root.tsxanimata/preloader/split-reveal/shutter.tsxanimata/preloader/split-reveal/task.tsxanimata/preloader/split-reveal/types.tsanimata/preloader/split-reveal/use-prefers-reduced-motion.tsanimata/preloader/split-reveal/use-scroll-lock.tsapp/demo/library/hero/photographer-portfolio-notes.tsxapp/demo/library/hero/photographer-portfolio.tsxcontent/docs/changelog/2026-06.mdxcontent/docs/preloader/index.mdxcontent/docs/preloader/split-reveal.mdxscripts/build-registry.js
✅ Files skipped from review due to trivial changes (5)
- animata/preloader/split-reveal/progress-slot.tsx
- animata/preloader/split-reveal/progress-track.tsx
- animata/preloader/split-reveal/types.ts
- app/demo/library/hero/photographer-portfolio-notes.tsx
- content/docs/changelog/2026-06.mdx
Move scroll lock and reduced-motion hooks to hooks/, consolidate on useLockBody, and skip re-bootstrap until the first run completes so Images task registration does not abort preload. Co-authored-by: Cursor <cursoragent@cursor.com>
Required for createContext and hooks when imported from server components. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace per-index HSL accents with hsl(var(--accent)) and theme tokens on the story front face for light and dark mode. Co-authored-by: Cursor <cursoragent@cursor.com>
Note shared hooks in split-reveal and card-stack install steps and drop the incorrect Marquee requirement from flipping-cards manual install. Co-authored-by: Cursor <cursoragent@cursor.com>
Expect use-lock-body and use-prefers-reduced-motion in split-reveal and card-stack shadcn dry-run output. Co-authored-by: Cursor <cursoragent@cursor.com>
Use --scope changed --base main so react-doctor 0.5.x runs without crashing. Co-authored-by: Cursor <cursoragent@cursor.com>
pnpm dlx @latest resolves to incompatible versions and conflicts with minimumReleaseAge; millionco/react-doctor@v2 uses npm exec instead. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/react-doctor.yml:
- Line 39: The GitHub Action reference `millionco/react-doctor@v2` on line 39
uses a floating tag which is vulnerable to upstream tag retargeting. Replace the
floating tag with a full commit SHA to pin the action to an immutable release,
and add an inline comment preserving the original tag version for readability.
This ensures the workflow cannot be compromised by tag reassignment or deletion
by the upstream maintainers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: efc4ec3c-aefb-40c6-9358-81ee1f91fd0f
📒 Files selected for processing (1)
.github/workflows/react-doctor.yml
Restore reduced-motion flip access, harden split-reveal lifecycle and task execution, use theme tokens and color-mix for progress text, and pin react-doctor action plus registry import parsing safeguards. Co-authored-by: Cursor <cursoragent@cursor.com>
Use the documented minimal checkout + action setup with PR feedback permissions, concurrency cancellation, and default v2 action inputs. Co-authored-by: Cursor <cursoragent@cursor.com>
|
React Doctor found 3 new issues in 1 file · 3 warnings · score 92 / 100 (Great) · 0 fixed · vs 3 warnings
Reviewed by React Doctor for commit |
Remove push-to-main trigger so the scan runs as a PR gate only. Co-authored-by: Cursor <cursoragent@cursor.com>
| SplitRevealTaskDefinition, | ||
| } from "./types"; | ||
|
|
||
| export function SplitRevealRoot({ |
There was a problem hiding this comment.
React Doctor · react-doctor/no-giant-component (warning)
Component "SplitRevealRoot" is 342 lines long, which is hard to read & change. Split it into a few smaller components.
Fix → Pull each section into its own component so the parent is easier to read, test, and change.
| controlledProgressRef.current = controlledProgress; | ||
| controlledReadyRef.current = controlledReady; | ||
| const maybeRevealRef = useRef<(() => void) | null>(null); | ||
| const [bootstrapEpoch, setBootstrapEpoch] = useState(0); |
There was a problem hiding this comment.
React Doctor · react-doctor/rerender-state-only-in-handlers (warning)
Each update to "bootstrapEpoch" redraws your component for nothing because this useState is set but never shown on screen.
Fix → Use useRef instead of useState when the value is only set and never shown on screen. ref.current = ... updates it without redrawing the component.
| const reduceMotion = usePrefersReducedMotion(); | ||
| const onCompleteRef = useRef(onComplete); | ||
| const phaseRef = useRef<PreloaderPhase>("loading"); | ||
| const tasksRef = useRef<Map<string, SplitRevealTaskDefinition>>(new Map()); |
There was a problem hiding this comment.
React Doctor · react-doctor/rerender-lazy-ref-init (warning)
useRef(new Map()) rebuilds this value on every render & throws it away.
Fix → Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.
Replace fixed image/title props with FlipCard.Front and FlipCard.Back. Consumers own face markup. Flip uses transform-only transitions with reduced-motion guards.
Summary by CodeRabbit
Front/Backfaces for X/Y flip customization.Images,Task,Overlay,Shutter, andProgress, including opt-in image preloading and externally-controlledready/progress.usePrefersReducedMotionfor motion-aware behavior.