Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions corpus/frontend/lenis/accessibility.yaml
Original file line number Diff line number Diff line change
@@ -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 (
<ReactLenis root options={{ lerp: 0.1 }}>
{children}
</ReactLenis>
);
}
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.
35 changes: 35 additions & 0 deletions corpus/frontend/lenis/custom-container.yaml
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);

return (
<ReactLenis
options={{
lerp: 0.1,
wrapper: wrapperRef.current ?? undefined,
content: contentRef.current ?? undefined,
}}
>
<div ref={wrapperRef} style={{ height: "100vh", overflow: "hidden" }}>
<div ref={contentRef}>
{children}
</div>
</div>
</ReactLenis>
);
}
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.
44 changes: 44 additions & 0 deletions corpus/frontend/lenis/framer-motion-integration.yaml
Original file line number Diff line number Diff line change
@@ -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 (
<ReactLenis root options={{ autoRaf: false, lerp: 0.1 }}>
<FramerSyncEffect />
{children}
</ReactLenis>
);
}
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."
25 changes: 25 additions & 0 deletions corpus/frontend/lenis/full-page.yaml
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<body>
<ReactLenis root options={{ lerp: 0.1 }}>
{children}
</ReactLenis>
</body>
</html>
);
}
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.
12 changes: 12 additions & 0 deletions corpus/frontend/lenis/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions corpus/frontend/lenis/next-js.yaml
Original file line number Diff line number Diff line change
@@ -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 (
<ReactLenis root options={{ lerp: 0.1, duration: 1.2 }}>
{children}
</ReactLenis>
);
}

// app/layout.tsx
import { SmoothScrollProvider } from "@/components/smooth-scroll-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SmoothScrollProvider>
{children}
</SmoothScrollProvider>
</body>
</html>
);
}
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.
41 changes: 41 additions & 0 deletions corpus/frontend/lenis/scroll-to-nav.yaml
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement>) {
e.preventDefault();
lenis?.scrollTo(href, {
offset,
duration,
easing: (t) => 1 - Math.pow(1 - t, 4),
});
}

return (
<a href={href} onClick={handleClick} className="nav-link">
{children}
</a>
);
}

// Usage:
// <NavLink href="#features" offset={-96}>Features</NavLink>
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.
52 changes: 49 additions & 3 deletions tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<ReactLenis root options={{ lerp: 0.1 }}>');
});

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");
});
Loading