Skip to content

feat(docs): add Resources nav dropdown and 4 new interactive blog tutorials#28

Merged
educlopez merged 2 commits intomainfrom
feat/resources-nav-blog-posts
Apr 15, 2026
Merged

feat(docs): add Resources nav dropdown and 4 new interactive blog tutorials#28
educlopez merged 2 commits intomainfrom
feat/resources-nav-blog-posts

Conversation

@educlopez
Copy link
Copy Markdown
Owner

@educlopez educlopez commented Apr 14, 2026

Summary

  • Refactor navbar (desktop + mobile) to group Blog, Sponsors, and Skills under a new Resources dropdown so the top-level bar stays tidy as we add more items.
  • Add ResourcesMenuIllustration with three animated sections (blog cards, sponsors heart, skills wand) matching the existing Components/Blocks preview pattern.
  • Add 4 new interactive tutorial blog posts following the existing scroll-triggered format:
    • Building a Magnetic Button with Cursor Physics
    • Building a Scramble-on-Hover Text Effect
    • Building Animated Tabs with a Shared Layout Indicator
    • Building a Number Flow Animation

Test plan

  • pnpm dev — verify Resources dropdown renders on desktop, illustration swaps between blog/sponsors/skills on hover
  • Verify mobile navbar shows the new Resources section
  • Open each of the 4 new blog posts and scroll through each step to confirm the live preview updates
  • Test prefers-reduced-motion — animations should be instant in all four tutorials
  • pnpm check passes

Summary by CodeRabbit

  • New Features

    • Added four interactive step-by-step tutorials with live previews: animated tabs, magnetic button, number flow, and scramble-on-hover.
    • Reworked navigation to a consolidated "Resources" menu (including blog, sponsors, skills) with an animated preview illustration and mobile adjustments.
  • Documentation

    • Published four new blog posts embedding each interactive tutorial and installation/key-takeaway notes.

…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
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
smoothui Ready Ready Preview, Comment Apr 14, 2026 6:46pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9ed8cba-7d62-427f-9f56-d998173bfa70

📥 Commits

Reviewing files that changed from the base of the PR and between 49bc058 and 83ebc39.

📒 Files selected for processing (1)
  • packages/smoothui/tsconfig.json
✅ Files skipped from review due to trivial changes (1)
  • packages/smoothui/tsconfig.json

Walkthrough

Adds 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

Cohort / File(s) Summary
Interactive Tutorial Components
apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx, apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx, apps/docs/components/blog/interactive-number-flow-tutorial.tsx, apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx
Added four client-side React tutorial components. Each uses IntersectionObserver to track scroll-synced steps, renders step-specific DynamicCodeBlock snippets, maintains a live preview with step-driven animations/interaction (tabs indicator, magnetic cursor, per-digit flows, scramble intervals), and includes reduced-motion/accessibility guards.
Navigation / Illustrations
apps/docs/components/landing/navbar/menu-illustration.tsx, apps/docs/components/landing/navbar/mobile-navbar.tsx, apps/docs/components/landing/navbar/navbar.tsx
Introduced ResourcesMenuIllustration (conditional animated SVG groups) and replaced individual Blog/Sponsors/Skills links with a Resources submenu in the main navbar. Mobile navbar groups resource links into a "Resources" section with motion wrappers. EnhancedListItem updated to support external links.
Blog Content (MDX)
apps/docs/content/blog/building-animated-tabs.mdx, apps/docs/content/blog/building-magnetic-button.mdx, apps/docs/content/blog/building-number-flow.mdx, apps/docs/content/blog/building-scramble-hover.mdx
Added four new MDX blog posts embedding the corresponding interactive tutorials, installer components, frontmatter metadata, and "Key Takeaways" guidance for each technique.
Package Config
packages/smoothui/tsconfig.json
Added a package-level tsconfig extending repo config, setting baseUrl, path aliases, and enabling strictNullChecks for the smoothui package.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(docs): add Resources nav dropdown and 4 new interactive blog tutorials' accurately summarizes the main changes: a new Resources navigation dropdown and four new interactive tutorial blog posts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/resources-nav-blog-posts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)} and ref to 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 with cursor-pointer) but lack onKeyDown and tabIndex for 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

📥 Commits

Reviewing files that changed from the base of the PR and between e21a8da and 49bc058.

📒 Files selected for processing (11)
  • apps/docs/components/blog/interactive-animated-tabs-tutorial.tsx
  • apps/docs/components/blog/interactive-magnetic-button-tutorial.tsx
  • apps/docs/components/blog/interactive-number-flow-tutorial.tsx
  • apps/docs/components/blog/interactive-scramble-hover-tutorial.tsx
  • apps/docs/components/landing/navbar/menu-illustration.tsx
  • apps/docs/components/landing/navbar/mobile-navbar.tsx
  • apps/docs/components/landing/navbar/navbar.tsx
  • apps/docs/content/blog/building-animated-tabs.mdx
  • apps/docs/content/blog/building-magnetic-button.mdx
  • apps/docs/content/blog/building-number-flow.mdx
  • apps/docs/content/blog/building-scramble-hover.mdx

Comment on lines +276 to +283
<motion.div
style={
showSpring
? { x: rawX, y: rawY }
: showPull
? { x: rawX.get(), y: rawY.get() }
: undefined
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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().

Comment on lines +1 to +6
"use client";

import { cn } from "@repo/shadcn-ui/lib/utils";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import { useEffect, useRef, useState } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +153 to +334
// 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>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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" }
+          }
         >
Based on learnings: MUST import and use useReducedMotion from motion/react in all animated components; MUST check shouldReduceMotion before applying animations; Respect prefers-reduced-motion in animations.
🤖 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.

Comment on lines 333 to +337
className="enhanced-list-item-link"
href={href}
onMouseEnter={onHover}
onMouseLeave={onLeave}
{...externalProps}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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}
       >
As per coding guidelines: include keyboard event handlers alongside mouse events.
🤖 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.

@educlopez educlopez merged commit b3de40a into main Apr 15, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant