feat(docs): add Resources nav dropdown and 4 new interactive blog tutorials#28
feat(docs): add Resources nav dropdown and 4 new interactive blog tutorials#28
Conversation
…orials - Refactor navbar (desktop + mobile) to group Blog, Sponsors, and Skills under a Resources dropdown - Add ResourcesMenuIllustration with animated sections (blog cards, sponsors heart, skills wand) - Add 4 new interactive tutorial blog posts: Magnetic Button, Scramble Hover, Animated Tabs, Number Flow
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughAdds four interactive client-side tutorial components with synchronized scroll-driven steps and live previews, four MDX blog posts embedding them, and restructures the navbar to add a "Resources" menu with an animated illustration and mobile adjustments. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant Browser as Browser (scroll / pointer)
participant Observer as IntersectionObserver
participant Tutorial as Tutorial Component
participant DOM as DOM (sections / indicator)
participant Preview as Live Preview
User->>Browser: scroll / interact
Browser->>Observer: step element crosses threshold
Observer->>Tutorial: notify active step
Tutorial->>Tutorial: update currentStep state
Tutorial->>DOM: measure active section bounding box
DOM-->>Tutorial: bounding box
Tutorial->>DOM: set indicator translateY
Tutorial->>Preview: enable/adjust preview interactions (tabs, magnet, digit animations, scramble)
User->>Preview: click / hover (preview)
Preview->>Tutorial: emit action (e.g., change tab, request scroll)
Tutorial->>DOM: scroll target section into view / update state
Preview->>Preview: run motion/spring transitions (respect reduced-motion)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
Matches the pattern used by other workspace packages (docs, data, shadcn-ui) and covers files at the smoothui package root (components/index.ts, hooks, utils) that aren't scoped by per-component tsconfigs.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
apps/docs/components/blog/interactive-number-flow-tutorial.tsx (1)
180-191: Add keyboard support for clickable step sections.Same accessibility pattern as the other tutorials — clickable sections need keyboard support.
♿ Proposed fix
<section className={cn( "relative cursor-pointer scroll-mt-32 rounded-xl p-4 transition-all hover:bg-muted/50", currentStep === step.id && "bg-muted/30" )} data-step={step.id} key={step.id} onClick={() => handleStepClick(step.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleStepClick(step.id); + } + }} ref={(el) => { if (el) stepRefs.current.set(step.id, el); }} + role="button" + tabIndex={0} >Based on learnings: "Ensure interactive elements are keyboard accessible".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/docs/components/blog/interactive-number-flow-tutorial.tsx` around lines 180 - 191, The clickable section is not keyboard accessible; update the section rendered in the map (the element using currentStep, step.id, stepRefs, and onClick calling handleStepClick) to behave like an interactive control by adding role="button", tabIndex={0}, an onKeyDown handler that calls handleStepClick(step.id) when Enter or Space is pressed, and ensure any focus/active styling remains consistent with the existing hover/selected styles; keep the existing onClick and ref logic (stepRefs.current.set) intact.apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx (2)
180-191: Add keyboard support for clickable step sections.Same accessibility issue as in the magnetic button tutorial — the
<section>elements are clickable but lack keyboard accessibility (tabIndex,onKeyDown,role).♿ Proposed fix
<section className={cn( "relative cursor-pointer scroll-mt-32 rounded-xl p-4 transition-all hover:bg-muted/50", currentStep === step.id && "bg-muted/30" )} data-step={step.id} key={step.id} onClick={() => handleStepClick(step.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleStepClick(step.id); + } + }} ref={(el) => { if (el) stepRefs.current.set(step.id, el); }} + role="button" + tabIndex={0} >Based on learnings: "Ensure interactive elements are keyboard accessible".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx` around lines 180 - 191, The clickable <section> lacks keyboard accessibility; add keyboard support by making it focusable and announcing it as interactive: set tabIndex=0 and role="button" on the <section>, and add an onKeyDown handler that listens for Enter and Space and calls handleStepClick(step.id) (same action as the onClick). Keep the existing onClick, ref logic, and use currentStep, step.id, stepRefs, and handleStepClick names so the behavior and focus styling remain consistent.
231-268: Consider adding keyboard navigation to the live preview tabs.The tutorial teaches keyboard navigation (Arrow keys, Home/End) in step 5, but the live preview tabs don't implement this behavior. This could be confusing for users following along — they see the code but can't test the interaction.
⌨️ Optional: Add keyboard navigation to preview tabs
+ const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map()); + + const handleTabKeyDown = (e: React.KeyboardEvent, index: number) => { + if (stepIndex < 4) return; // Only enable after keyboard step + let nextIndex = index; + if (e.key === "ArrowRight") nextIndex = (index + 1) % TABS.length; + else if (e.key === "ArrowLeft") nextIndex = (index - 1 + TABS.length) % TABS.length; + else if (e.key === "Home") nextIndex = 0; + else if (e.key === "End") nextIndex = TABS.length - 1; + else return; + e.preventDefault(); + const nextTab = TABS[nextIndex]; + setActive(nextTab.id); + tabRefs.current.get(nextTab.id)?.focus(); + };Then add
onKeyDown={(e) => handleTabKeyDown(e, index)}andrefto each tab button.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx` around lines 231 - 268, Add keyboard navigation by creating a tabRefs collection (e.g., useRef<HTMLButtonElement[]>([])) and a handler function handleTabKeyDown(event, index) that handles ArrowRight/ArrowLeft to move focus and call setActive for next/previous tabs, and Home/End to focus and activate first/last; then attach onKeyDown={(e) => handleTabKeyDown(e, index)} and set a ref on each button (ref={el => (tabRefs.current[index] = el)}) inside the TABS.map loop, keeping existing symbols: TABS, active, setActive, showIndicator, showActiveState; also ensure the tab container uses role="tablist" so the role="tab" buttons are correctly grouped.apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx (1)
223-234: Add keyboard support for clickable step sections.The
<section>elements are interactive (clickable withcursor-pointer) but lackonKeyDownandtabIndexfor keyboard users. Screen reader users and keyboard-only users cannot navigate to or activate these steps.♿ Proposed fix to add keyboard accessibility
<section className={cn( "relative cursor-pointer scroll-mt-32 rounded-xl p-4 transition-all hover:bg-muted/50", currentStep === step.id && "bg-muted/30" )} data-active={currentStep === step.id} data-step={step.id} key={step.id} onClick={() => handleStepClick(step.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleStepClick(step.id); + } + }} ref={(el) => { if (el) stepRefs.current.set(step.id, el); }} + role="button" + tabIndex={0} >Based on learnings: "Ensure interactive elements are keyboard accessible".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx` around lines 223 - 234, The section elements used for steps are clickable but not keyboard-accessible; update the interactive <section> (the element that uses currentStep, step.id, handleStepClick and stepRefs) to include tabIndex={0}, add an onKeyDown handler that calls handleStepClick(step.id) when Enter or Space is pressed, and set an appropriate accessibility role/aria attribute (e.g., role="button" and aria-pressed or aria-selected based on currentStep === step.id); ensure the onKeyDown handler prevents default for Space to avoid page scroll and keep existing onClick and ref handling intact.apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx (1)
211-221: Add keyboard support for clickable step sections.Same accessibility pattern as the other tutorials — clickable sections need keyboard support.
♿ Proposed fix
<section className={cn( "relative cursor-pointer scroll-mt-32 rounded-xl p-4 transition-all hover:bg-muted/50", currentStep === step.id && "bg-muted/30" )} data-step={step.id} key={step.id} onClick={() => handleStepClick(step.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleStepClick(step.id); + } + }} ref={(el) => { if (el) stepRefs.current.set(step.id, el); }} + role="button" + tabIndex={0} >Based on learnings: "Ensure interactive elements are keyboard accessible".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx` around lines 211 - 221, The step <section> is clickable but not keyboard-accessible; make it focusable and respond to Enter/Space keys by adding tabIndex={0}, role="button", and an onKeyDown handler that calls handleStepClick(step.id) when Enter or Space is pressed (preventDefault for Space), and add aria-current or similar when currentStep === step.id to convey active state; keep the existing ref assignment to stepRefs.current and the onClick handler intact so handleStepClick is used consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx`:
- Around line 276-283: The motion div currently uses rawX.get()/rawY.get() in
the showPull branch which captures static values and prevents reactive updates;
change the style prop to pass motion values (not .get()) so Framer Motion can
update them reactively — e.g., use { x: rawX, y: rawY } for pull or always use
the spring motion values (springX/springY) for both modes; update the
conditional around showSpring/showPull in the motion.div to return motion values
(rawX/rawY or springX/springY) instead of calling .get().
In `@apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx`:
- Around line 1-6: Import useReducedMotion from "motion/react" and use it inside
the InteractiveScrambleHoverTutorial component: add const shouldReduceMotion =
useReducedMotion(); then guard all animated behavior (any timers,
requestAnimationFrame loops, animation props passed to DynamicCodeBlock, CSS
animation durations, scramble/hover animation functions) to either no-op or use
minimal/zero duration when shouldReduceMotion is true so the live preview
respects prefers-reduced-motion; ensure the import name useReducedMotion and the
boolean shouldReduceMotion are used wherever animations are started or animation
props are computed.
In `@apps/docs/components/landing/navbar/menu-illustration.tsx`:
- Around line 153-334: Import and call useReducedMotion (from motion/react per
review) inside ResourcesMenuIllustration and guard all animated props on
motion.svg and the three motion.g blocks (the Blog, Sponsors, Skills groups): if
shouldReduceMotion is true, remove/avoid animated transitions by applying static
values (set initial/animate to the final state or omit transition / set duration
0) so opacity/transform changes are instantaneous; otherwise keep the existing
initial/animate/transition objects. Update every reference to animate, initial,
and transition on motion.svg and the motion.g elements to respect
shouldReduceMotion.
In `@apps/docs/components/landing/navbar/navbar.tsx`:
- Around line 333-337: The hover preview only updates on mouse events; add
keyboard parity by wiring focus events to the same handlers—update the element
with onFocus={onHover} and onBlur={onLeave} (or call the same handler functions
used for onMouseEnter/onMouseLeave) so keyboard/tab users trigger the same
preview behavior; modify the JSX element with className
"enhanced-list-item-link" (the element using href, onMouseEnter, onMouseLeave,
and externalProps) to include these focus handlers and ensure they receive the
same event data as the mouse handlers.
---
Nitpick comments:
In `@apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx`:
- Around line 180-191: The clickable <section> lacks keyboard accessibility; add
keyboard support by making it focusable and announcing it as interactive: set
tabIndex=0 and role="button" on the <section>, and add an onKeyDown handler that
listens for Enter and Space and calls handleStepClick(step.id) (same action as
the onClick). Keep the existing onClick, ref logic, and use currentStep,
step.id, stepRefs, and handleStepClick names so the behavior and focus styling
remain consistent.
- Around line 231-268: Add keyboard navigation by creating a tabRefs collection
(e.g., useRef<HTMLButtonElement[]>([])) and a handler function
handleTabKeyDown(event, index) that handles ArrowRight/ArrowLeft to move focus
and call setActive for next/previous tabs, and Home/End to focus and activate
first/last; then attach onKeyDown={(e) => handleTabKeyDown(e, index)} and set a
ref on each button (ref={el => (tabRefs.current[index] = el)}) inside the
TABS.map loop, keeping existing symbols: TABS, active, setActive, showIndicator,
showActiveState; also ensure the tab container uses role="tablist" so the
role="tab" buttons are correctly grouped.
In `@apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx`:
- Around line 223-234: The section elements used for steps are clickable but not
keyboard-accessible; update the interactive <section> (the element that uses
currentStep, step.id, handleStepClick and stepRefs) to include tabIndex={0}, add
an onKeyDown handler that calls handleStepClick(step.id) when Enter or Space is
pressed, and set an appropriate accessibility role/aria attribute (e.g.,
role="button" and aria-pressed or aria-selected based on currentStep ===
step.id); ensure the onKeyDown handler prevents default for Space to avoid page
scroll and keep existing onClick and ref handling intact.
In `@apps/docs/components/blog/interactive-number-flow-tutorial.tsx`:
- Around line 180-191: The clickable section is not keyboard accessible; update
the section rendered in the map (the element using currentStep, step.id,
stepRefs, and onClick calling handleStepClick) to behave like an interactive
control by adding role="button", tabIndex={0}, an onKeyDown handler that calls
handleStepClick(step.id) when Enter or Space is pressed, and ensure any
focus/active styling remains consistent with the existing hover/selected styles;
keep the existing onClick and ref logic (stepRefs.current.set) intact.
In `@apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx`:
- Around line 211-221: The step <section> is clickable but not
keyboard-accessible; make it focusable and respond to Enter/Space keys by adding
tabIndex={0}, role="button", and an onKeyDown handler that calls
handleStepClick(step.id) when Enter or Space is pressed (preventDefault for
Space), and add aria-current or similar when currentStep === step.id to convey
active state; keep the existing ref assignment to stepRefs.current and the
onClick handler intact so handleStepClick is used consistently.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ec1f5234-aa12-4edf-8e74-b8cb7ee99010
📒 Files selected for processing (11)
apps/docs/components/blog/interactive-animated-tabs-tutorial.tsxapps/docs/components/blog/interactive-magnetic-button-tutorial.tsxapps/docs/components/blog/interactive-number-flow-tutorial.tsxapps/docs/components/blog/interactive-scramble-hover-tutorial.tsxapps/docs/components/landing/navbar/menu-illustration.tsxapps/docs/components/landing/navbar/mobile-navbar.tsxapps/docs/components/landing/navbar/navbar.tsxapps/docs/content/blog/building-animated-tabs.mdxapps/docs/content/blog/building-magnetic-button.mdxapps/docs/content/blog/building-number-flow.mdxapps/docs/content/blog/building-scramble-hover.mdx
| <motion.div | ||
| style={ | ||
| showSpring | ||
| ? { x: rawX, y: rawY } | ||
| : showPull | ||
| ? { x: rawX.get(), y: rawY.get() } | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
Non-reactive motion values in non-spring mode.
When showSpring is false but showPull is true (step 3), using rawX.get() and rawY.get() returns static values at render time. The button won't animate smoothly because these values won't update reactively — they're captured once when the component renders.
Consider always using the spring motion values and controlling animation via the spring config, or ensure the component re-renders on mouse move (which it does via state changes). Since handleMouseMove calls rawX.set() and rawY.set(), React won't re-render because motion values don't trigger re-renders.
🔧 Proposed fix
<motion.div
style={
- showSpring
- ? { x: rawX, y: rawY }
- : showPull
- ? { x: rawX.get(), y: rawY.get() }
- : undefined
+ showPull ? { x: rawX, y: rawY } : undefined
}
>The spring motion values will still work correctly for the "pull" step — the spring config just adds smoother interpolation. This simplifies the logic and ensures reactive updates.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <motion.div | |
| style={ | |
| showSpring | |
| ? { x: rawX, y: rawY } | |
| : showPull | |
| ? { x: rawX.get(), y: rawY.get() } | |
| : undefined | |
| } | |
| <motion.div | |
| style={ | |
| showSpring || showPull | |
| ? { x: rawX, y: rawY } | |
| : undefined | |
| } | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx` around
lines 276 - 283, The motion div currently uses rawX.get()/rawY.get() in the
showPull branch which captures static values and prevents reactive updates;
change the style prop to pass motion values (not .get()) so Framer Motion can
update them reactively — e.g., use { x: rawX, y: rawY } for pull or always use
the spring motion values (springX/springY) for both modes; update the
conditional around showSpring/showPull in the motion.div to return motion values
(rawX/rawY or springX/springY) instead of calling .get().
| "use client"; | ||
|
|
||
| import { cn } from "@repo/shadcn-ui/lib/utils"; | ||
| import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
There was a problem hiding this comment.
Missing useReducedMotion import and implementation.
The tutorial has an "Accessibility" step that teaches prefers-reduced-motion handling, but the live preview doesn't actually respect reduced motion preferences. This creates an inconsistency where the tutorial teaches best practices but doesn't follow them.
♿ Proposed fix to implement reduced motion support
"use client";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
+import { useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";Then in the component:
export function InteractiveScrambleHoverTutorial({
className,
}: InteractiveScrambleHoverTutorialProps) {
const ORIGINAL = "Scramble Me";
const [currentStep, setCurrentStep] = useState<StepId>("base");
const [display, setDisplay] = useState(ORIGINAL);
+ const shouldReduceMotion = useReducedMotion();
const stepRefs = useRef<Map<string, HTMLElement>>(new Map());
// ...
const stepIndex = STEPS.findIndex((s) => s.id === currentStep);
const canScramble = stepIndex >= 2;
const hasTimeout = stepIndex >= 3;
+ const respectReducedMotion = stepIndex >= 5;
+ const isDisabled = respectReducedMotion && shouldReduceMotion;
const handleEnter = () => {
- if (!canScramble) return;
+ if (!canScramble || isDisabled) return;
// rest of implementation...
};Based on learnings: "MUST import and use useReducedMotion from motion/react in all animated components" and "MUST check shouldReduceMotion before applying animations".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx` around
lines 1 - 6, Import useReducedMotion from "motion/react" and use it inside the
InteractiveScrambleHoverTutorial component: add const shouldReduceMotion =
useReducedMotion(); then guard all animated behavior (any timers,
requestAnimationFrame loops, animation props passed to DynamicCodeBlock, CSS
animation durations, scramble/hover animation functions) to either no-op or use
minimal/zero duration when shouldReduceMotion is true so the live preview
respects prefers-reduced-motion; ensure the import name useReducedMotion and the
boolean shouldReduceMotion are used wherever animations are started or animation
props are computed.
| // Resources MenuIllustration - for blog, sponsors, skills | ||
| export function ResourcesMenuIllustration({ | ||
| activeSection, | ||
| className = "", | ||
| }: MenuIllustrationProps) { | ||
| return ( | ||
| <motion.svg | ||
| animate={{ opacity: 1 }} | ||
| aria-label={`Resources menu illustration for ${activeSection} section`} | ||
| className={cn(className, "overflow-hidden rounded-md")} | ||
| fill="none" | ||
| height="231" | ||
| initial={{ opacity: 0 }} | ||
| role="img" | ||
| style={{ overflow: "hidden" }} | ||
| transition={{ duration: 0.3, ease: "easeOut" }} | ||
| viewBox="0 0 231 231" | ||
| width="231" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <defs> | ||
| <clipPath id="clip0_resources"> | ||
| <rect height="231" rx="7.22" width="231" /> | ||
| </clipPath> | ||
| </defs> | ||
| <g clipPath="url(#clip0_resources)"> | ||
| <rect | ||
| className="fill-brand-secondary" | ||
| height="231" | ||
| rx="7.22" | ||
| width="231" | ||
| /> | ||
|
|
||
| {/* Blog — stacked article cards */} | ||
| <motion.g | ||
| animate={{ | ||
| opacity: activeSection === "blog" ? 1 : 0, | ||
| y: activeSection === "blog" ? 0 : 20, | ||
| }} | ||
| initial={{ opacity: 0, y: 20 }} | ||
| transition={{ duration: 0.4, ease: "easeOut" }} | ||
| > | ||
| <rect | ||
| height="55" | ||
| rx="10" | ||
| style={{ fill: "var(--color-brand-lighter)" }} | ||
| width="260" | ||
| x="-15" | ||
| y="24" | ||
| /> | ||
| <rect | ||
| height="12" | ||
| rx="6" | ||
| style={{ fill: "var(--color-brand)" }} | ||
| width="120" | ||
| x="20" | ||
| y="38" | ||
| /> | ||
| <rect | ||
| height="8" | ||
| rx="4" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| width="180" | ||
| x="20" | ||
| y="58" | ||
| /> | ||
| <rect | ||
| height="55" | ||
| rx="10" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| width="260" | ||
| x="-15" | ||
| y="88" | ||
| /> | ||
| <rect | ||
| height="12" | ||
| rx="6" | ||
| style={{ fill: "var(--color-brand)" }} | ||
| width="150" | ||
| x="20" | ||
| y="102" | ||
| /> | ||
| <rect | ||
| height="8" | ||
| rx="4" | ||
| style={{ fill: "var(--color-brand-lighter)" }} | ||
| width="160" | ||
| x="20" | ||
| y="122" | ||
| /> | ||
| <rect | ||
| height="55" | ||
| rx="10" | ||
| style={{ fill: "var(--color-brand-lighter)" }} | ||
| width="260" | ||
| x="-15" | ||
| y="152" | ||
| /> | ||
| <rect | ||
| height="12" | ||
| rx="6" | ||
| style={{ fill: "var(--color-brand)" }} | ||
| width="100" | ||
| x="20" | ||
| y="166" | ||
| /> | ||
| <rect | ||
| height="8" | ||
| rx="4" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| width="190" | ||
| x="20" | ||
| y="186" | ||
| /> | ||
| </motion.g> | ||
|
|
||
| {/* Sponsors — big heart */} | ||
| <motion.g | ||
| animate={{ | ||
| opacity: activeSection === "sponsors" ? 1 : 0, | ||
| scale: activeSection === "sponsors" ? 1 : 0.9, | ||
| }} | ||
| initial={{ opacity: 0, scale: 0.9 }} | ||
| style={{ transformOrigin: "115.5px 115.5px" }} | ||
| transition={{ duration: 0.4, ease: "easeOut" }} | ||
| > | ||
| <path | ||
| d="M115.5 185 C 60 150, 30 115, 30 80 C 30 55, 50 38, 75 38 C 92 38, 108 48, 115.5 62 C 123 48, 139 38, 156 38 C 181 38, 201 55, 201 80 C 201 115, 171 150, 115.5 185 Z" | ||
| style={{ fill: "var(--color-brand)" }} | ||
| /> | ||
| <path | ||
| d="M115.5 165 C 75 137, 50 110, 50 83 C 50 68, 62 57, 78 57 C 92 57, 105 66, 115.5 80 C 126 66, 139 57, 153 57 C 169 57, 181 68, 181 83 C 181 110, 156 137, 115.5 165 Z" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| /> | ||
| </motion.g> | ||
|
|
||
| {/* Skills — wand + sparkles (centered on canvas diagonal) */} | ||
| <motion.g | ||
| animate={{ | ||
| opacity: activeSection === "skills" ? 1 : 0, | ||
| rotate: activeSection === "skills" ? 0 : -8, | ||
| }} | ||
| initial={{ opacity: 0, rotate: -8 }} | ||
| style={{ transformOrigin: "115.5px 115.5px" }} | ||
| transition={{ duration: 0.4, ease: "easeOut" }} | ||
| > | ||
| {/* Wand shaft — centered bar rotated on the canvas diagonal */} | ||
| <rect | ||
| height="18" | ||
| rx="9" | ||
| style={{ fill: "var(--color-brand)" }} | ||
| transform="rotate(-45 115.5 115.5)" | ||
| width="150" | ||
| x="40.5" | ||
| y="106.5" | ||
| /> | ||
| {/* Wand tip sparkle (top-right end) */} | ||
| <path | ||
| d="M167 60 L174 78 L192 85 L174 92 L167 110 L160 92 L142 85 L160 78 Z" | ||
| style={{ fill: "var(--color-brand-lighter)" }} | ||
| /> | ||
| {/* Handle dot (bottom-left end) */} | ||
| <circle | ||
| cx="64" | ||
| cy="167" | ||
| r="10" | ||
| style={{ fill: "var(--color-brand-lighter)" }} | ||
| /> | ||
| {/* Balanced accent sparkles */} | ||
| <path | ||
| d="M58 58 L62 70 L74 74 L62 78 L58 90 L54 78 L42 74 L54 70 Z" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| /> | ||
| <path | ||
| d="M173 155 L176 164 L185 167 L176 170 L173 179 L170 170 L161 167 L170 164 Z" | ||
| style={{ fill: "var(--color-brand-light)" }} | ||
| /> | ||
| </motion.g> | ||
| </g> | ||
| </motion.svg> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Respect reduced-motion in ResourcesMenuIllustration animations.
This new animated component currently always animates opacity/transform transitions. It should gate animations with useReducedMotion and switch to instant transitions when reduction is requested.
🛠️ Suggested fix
-import { motion } from "motion/react";
+import { motion, useReducedMotion } from "motion/react";
...
export function ResourcesMenuIllustration({
activeSection,
className = "",
}: MenuIllustrationProps) {
+ const shouldReduceMotion = useReducedMotion();
+
return (
<motion.svg
animate={{ opacity: 1 }}
...
- transition={{ duration: 0.3, ease: "easeOut" }}
+ transition={
+ shouldReduceMotion ? { duration: 0 } : { duration: 0.3, ease: "easeOut" }
+ }
...
<motion.g
animate={{
opacity: activeSection === "blog" ? 1 : 0,
- y: activeSection === "blog" ? 0 : 20,
+ y: shouldReduceMotion ? 0 : activeSection === "blog" ? 0 : 20,
}}
- initial={{ opacity: 0, y: 20 }}
- transition={{ duration: 0.4, ease: "easeOut" }}
+ initial={shouldReduceMotion ? { opacity: 0, y: 0 } : { opacity: 0, y: 20 }}
+ transition={
+ shouldReduceMotion ? { duration: 0 } : { duration: 0.4, ease: "easeOut" }
+ }
>
...
<motion.g
animate={{
opacity: activeSection === "sponsors" ? 1 : 0,
- scale: activeSection === "sponsors" ? 1 : 0.9,
+ scale: shouldReduceMotion ? 1 : activeSection === "sponsors" ? 1 : 0.9,
}}
- initial={{ opacity: 0, scale: 0.9 }}
+ initial={shouldReduceMotion ? { opacity: 0, scale: 1 } : { opacity: 0, scale: 0.9 }}
style={{ transformOrigin: "115.5px 115.5px" }}
- transition={{ duration: 0.4, ease: "easeOut" }}
+ transition={
+ shouldReduceMotion ? { duration: 0 } : { duration: 0.4, ease: "easeOut" }
+ }
>
...
<motion.g
animate={{
opacity: activeSection === "skills" ? 1 : 0,
- rotate: activeSection === "skills" ? 0 : -8,
+ rotate: shouldReduceMotion ? 0 : activeSection === "skills" ? 0 : -8,
}}
- initial={{ opacity: 0, rotate: -8 }}
+ initial={shouldReduceMotion ? { opacity: 0, rotate: 0 } : { opacity: 0, rotate: -8 }}
style={{ transformOrigin: "115.5px 115.5px" }}
- transition={{ duration: 0.4, ease: "easeOut" }}
+ transition={
+ shouldReduceMotion ? { duration: 0 } : { duration: 0.4, ease: "easeOut" }
+ }
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/docs/components/landing/navbar/menu-illustration.tsx` around lines 153 -
334, Import and call useReducedMotion (from motion/react per review) inside
ResourcesMenuIllustration and guard all animated props on motion.svg and the
three motion.g blocks (the Blog, Sponsors, Skills groups): if shouldReduceMotion
is true, remove/avoid animated transitions by applying static values (set
initial/animate to the final state or omit transition / set duration 0) so
opacity/transform changes are instantaneous; otherwise keep the existing
initial/animate/transition objects. Update every reference to animate, initial,
and transition on motion.svg and the motion.g elements to respect
shouldReduceMotion.
| className="enhanced-list-item-link" | ||
| href={href} | ||
| onMouseEnter={onHover} | ||
| onMouseLeave={onLeave} | ||
| {...externalProps} |
There was a problem hiding this comment.
Add keyboard focus parity for hover preview behavior.
onMouseEnter/onMouseLeave are wired, but focus events are missing, so keyboard users don’t get equivalent preview updates.
♿ Suggested fix
<Link
className="enhanced-list-item-link"
href={href}
onMouseEnter={onHover}
onMouseLeave={onLeave}
+ onFocus={onHover}
+ onBlur={onLeave}
{...externalProps}
{...props}
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/docs/components/landing/navbar/navbar.tsx` around lines 333 - 337, The
hover preview only updates on mouse events; add keyboard parity by wiring focus
events to the same handlers—update the element with onFocus={onHover} and
onBlur={onLeave} (or call the same handler functions used for
onMouseEnter/onMouseLeave) so keyboard/tab users trigger the same preview
behavior; modify the JSX element with className "enhanced-list-item-link" (the
element using href, onMouseEnter, onMouseLeave, and externalProps) to include
these focus handlers and ensure they receive the same event data as the mouse
handlers.
Summary
ResourcesMenuIllustrationwith three animated sections (blog cards, sponsors heart, skills wand) matching the existing Components/Blocks preview pattern.Test plan
pnpm dev— verify Resources dropdown renders on desktop, illustration swaps between blog/sponsors/skills on hoverprefers-reduced-motion— animations should be instant in all four tutorialspnpm checkpassesSummary by CodeRabbit
New Features
Documentation