diff --git a/corpus/frontend/lenis/accessibility.yaml b/corpus/frontend/lenis/accessibility.yaml new file mode 100644 index 0000000..37510b3 --- /dev/null +++ b/corpus/frontend/lenis/accessibility.yaml @@ -0,0 +1,32 @@ +name: accessibility +description: >- + Respect prefers-reduced-motion by disabling smooth scrolling for users who + prefer it. +code: | + "use client"; + + import { ReactLenis } from "lenis/react"; + import "lenis/dist/lenis.css"; + + function useReducedMotion() { + if (typeof window === "undefined") return false; + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } + + export function AccessibleSmoothScrollProvider({ children }: { children: React.ReactNode }) { + const prefersReducedMotion = useReducedMotion(); + + if (prefersReducedMotion) { + return <>{children}; + } + + return ( + + {children} + + ); + } +tips: + - Never force smooth scrolling on users who have opted out via prefers-reduced-motion. + - When skipping ReactLenis, native scroll is used - no polyfill needed. + - For a hook-based approach, use the useReducedMotion hook from Framer Motion or write your own with a useEffect + matchMedia listener. diff --git a/corpus/frontend/lenis/custom-container.yaml b/corpus/frontend/lenis/custom-container.yaml new file mode 100644 index 0000000..07e8955 --- /dev/null +++ b/corpus/frontend/lenis/custom-container.yaml @@ -0,0 +1,35 @@ +name: custom-container +description: >- + Scoped scroll container using wrapper and content refs for non-window smooth + scroll. +code: | + "use client"; + + import { useRef } from "react"; + import { ReactLenis } from "lenis/react"; + import "lenis/dist/lenis.css"; + + export function ScrollPanel({ children }: { children: React.ReactNode }) { + const wrapperRef = useRef(null); + const contentRef = useRef(null); + + return ( + +
+
+ {children} +
+
+
+ ); + } +tips: + - "The wrapper element needs overflow: hidden and a fixed height for container scroll to work." + - The content element is the scrollable inner div - it should grow naturally with its children. + - This pattern is useful for split-panel layouts, side drawers, or modal scroll areas. diff --git a/corpus/frontend/lenis/framer-motion-integration.yaml b/corpus/frontend/lenis/framer-motion-integration.yaml new file mode 100644 index 0000000..6ec978c --- /dev/null +++ b/corpus/frontend/lenis/framer-motion-integration.yaml @@ -0,0 +1,44 @@ +name: framer-motion-integration +description: >- + Integrate Lenis with Framer Motion by disabling autoRaf and syncing via + frame.update. +code: | + "use client"; + + import { useEffect } from "react"; + import { ReactLenis, useLenis } from "lenis/react"; + import "lenis/dist/lenis.css"; + import { frame } from "motion"; + + function FramerSyncEffect() { + const lenis = useLenis(); + + useEffect(() => { + if (!lenis) return; + + function update({ timestamp }: { timestamp: number }) { + lenis.raf(timestamp); + } + + frame.update(update, true); + + return () => { + frame.cancel(update); + }; + }, [lenis]); + + return null; + } + + export function FramerLenisProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); + } +tips: + - "Use frame from 'motion' (not 'framer-motion') - this is the Framer Motion v11+ low-level scheduler." + - "frame.update(fn, true) schedules the update to run on every frame. The second argument (true) enables loop mode." + - "autoRaf: false is mandatory - same reasoning as with GSAP, prevent double-ticking." diff --git a/corpus/frontend/lenis/full-page.yaml b/corpus/frontend/lenis/full-page.yaml new file mode 100644 index 0000000..64a6b65 --- /dev/null +++ b/corpus/frontend/lenis/full-page.yaml @@ -0,0 +1,25 @@ +name: full-page +description: >- + Standard root layout setup - ReactLenis wraps the entire app for full-page + smooth scrolling. +code: | + // app/layout.tsx (Next.js App Router) + "use client"; // Must be client component + + import { ReactLenis } from "lenis/react"; + import "lenis/dist/lenis.css"; + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); + } +tips: + - "root={true} is required for full-page scroll - without it, Lenis creates an overflow:hidden container." + - The CSS import is mandatory - skip it and the layout breaks. diff --git a/corpus/frontend/lenis/index.yaml b/corpus/frontend/lenis/index.yaml index a9f0f74..1a865b8 100644 --- a/corpus/frontend/lenis/index.yaml +++ b/corpus/frontend/lenis/index.yaml @@ -2,3 +2,15 @@ namespace: frontend.lenis patterns: gsap-integration: file: gsap-integration.yaml + full-page: + file: full-page.yaml + next-js: + file: next-js.yaml + framer-motion-integration: + file: framer-motion-integration.yaml + custom-container: + file: custom-container.yaml + scroll-to-nav: + file: scroll-to-nav.yaml + accessibility: + file: accessibility.yaml diff --git a/corpus/frontend/lenis/next-js.yaml b/corpus/frontend/lenis/next-js.yaml new file mode 100644 index 0000000..6f63ed1 --- /dev/null +++ b/corpus/frontend/lenis/next-js.yaml @@ -0,0 +1,36 @@ +name: next-js +description: >- + Next.js App Router pattern using a dedicated SmoothScrollProvider client + component to wrap the layout. +code: | + // components/smooth-scroll-provider.tsx + "use client"; + + import { ReactLenis } from "lenis/react"; + import "lenis/dist/lenis.css"; + + export function SmoothScrollProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + // app/layout.tsx + import { SmoothScrollProvider } from "@/components/smooth-scroll-provider"; + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); + } +tips: + - "Keep app/layout.tsx as a Server Component - extract the 'use client' directive into SmoothScrollProvider." + - This preserves RSC boundaries and avoids unnecessarily client-rendering the entire layout. diff --git a/corpus/frontend/lenis/scroll-to-nav.yaml b/corpus/frontend/lenis/scroll-to-nav.yaml new file mode 100644 index 0000000..575d074 --- /dev/null +++ b/corpus/frontend/lenis/scroll-to-nav.yaml @@ -0,0 +1,41 @@ +name: scroll-to-nav +description: >- + Navigation link that uses lenis.scrollTo() for smooth in-page anchor + navigation. +code: | + "use client"; + + import { useLenis } from "lenis/react"; + + interface NavLinkProps { + href: string; + children: React.ReactNode; + offset?: number; + duration?: number; + } + + export function NavLink({ href, children, offset = -80, duration = 1.2 }: NavLinkProps) { + const lenis = useLenis(); + + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + lenis?.scrollTo(href, { + offset, + duration, + easing: (t) => 1 - Math.pow(1 - t, 4), + }); + } + + return ( + + {children} + + ); + } + + // Usage: + // Features +tips: + - offset compensates for sticky headers - pass a negative value equal to the header height. + - "lenis.scrollTo() accepts a CSS selector ('#section'), HTMLElement, or pixel number." + - For Next.js App Router, use usePathname to detect route changes and reset scroll position. diff --git a/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts index a039f55..f704e3f 100644 --- a/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts +++ b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts @@ -13,11 +13,57 @@ test("lenis_get_pattern prefers corpus metadata for gsap-integration", async () expect(text).toContain('import { ReactLenis, useLenis } from "lenis/react"'); }); -test("lenis_get_pattern falls back to in-file data for non-corpus patterns", async () => { +test("lenis_get_pattern prefers corpus metadata for full-page", async () => { + const result = await lenisGetPattern.invoke({ name: "full-page" }); + const text = extractTextContent(result); + + expect(text).toContain("# Lenis Pattern: full-page"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain(''); +}); + +test("lenis_get_pattern prefers corpus metadata for next-js", async () => { + const result = await lenisGetPattern.invoke({ name: "next-js" }); + const text = extractTextContent(result); + + expect(text).toContain("# Lenis Pattern: next-js"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain("SmoothScrollProvider"); +}); + +test("lenis_get_pattern prefers corpus metadata for framer-motion-integration", async () => { + const result = await lenisGetPattern.invoke({ name: "framer-motion-integration" }); + const text = extractTextContent(result); + + expect(text).toContain("# Lenis Pattern: framer-motion-integration"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain('import { frame } from "motion";'); +}); + +test("lenis_get_pattern prefers corpus metadata for custom-container", async () => { + const result = await lenisGetPattern.invoke({ name: "custom-container" }); + const text = extractTextContent(result); + + expect(text).toContain("# Lenis Pattern: custom-container"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain("ScrollPanel"); +}); + +test("lenis_get_pattern prefers corpus metadata for scroll-to-nav", async () => { + const result = await lenisGetPattern.invoke({ name: "scroll-to-nav" }); + const text = extractTextContent(result); + + expect(text).toContain("# Lenis Pattern: scroll-to-nav"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain("lenis?.scrollTo(href"); +}); + +test("lenis_get_pattern prefers corpus metadata for accessibility", async () => { const result = await lenisGetPattern.invoke({ name: "accessibility" }); const text = extractTextContent(result); expect(text).toContain("# Lenis Pattern: accessibility"); - expect(text).toContain("## Key Notes"); - expect(text).not.toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain("**Corpus Source:** frontend.lenis"); + expect(text).toContain("prefers-reduced-motion"); + expect(text).toContain("AccessibleSmoothScrollProvider"); });