UX overhaul: cohesive navigation + sticky header, new sidebar menu with smooth anchor scrolling, home layout restructured into cards (About/Writing/Contact), blog list affordance improved, background dot contrast reduced, Lenis tuned for softer feel, footer aligned to page container, Next/Image sizing warning fixed, providers (Lenis/Motion) wired.

This commit is contained in:
nicholai 2025-10-07 19:08:57 -06:00
parent 4b1b6ec6cb
commit ef24c27085
12 changed files with 416 additions and 151 deletions

View File

@ -32,6 +32,8 @@ export function AvatarMotion({
alt={alt} alt={alt}
width={size} width={size}
height={size} height={size}
sizes={`${size}px`}
style={{ width: size, height: size }}
className={cn( className={cn(
baseSizeClasses, baseSizeClasses,
"rounded-full object-cover ring-2 ring-neutral-200 shadow-lg transition-shadow duration-300 group-hover:shadow-xl dark:shadow-none dark:ring-neutral-800", "rounded-full object-cover ring-2 ring-neutral-200 shadow-lg transition-shadow duration-300 group-hover:shadow-xl dark:shadow-none dark:ring-neutral-800",

View File

@ -12,6 +12,7 @@ export function DotBackground({ className }: { className?: string }) {
"[background-size:28px_28px]", "[background-size:28px_28px]",
"[background-image:radial-gradient(var(--dot-color)_1px,transparent_1px)]", "[background-image:radial-gradient(var(--dot-color)_1px,transparent_1px)]",
"dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]", "dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]",
"opacity-40 dark:opacity-20",
)} )}
/> />
{/* Radial gradient for the container to give a faded look */} {/* Radial gradient for the container to give a faded look */}

View File

@ -17,8 +17,9 @@ function ArrowIcon() {
export default function Footer() { export default function Footer() {
return ( return (
<footer className="mb-16"> <footer className="mt-16 border-t border-black/5 dark:border-white/10">
<ul className="font-sm mt-8 flex flex-col space-x-0 space-y-2 text-neutral-600 md:flex-row md:space-x-4 md:space-y-0 dark:text-neutral-300"> <div className="mx-auto max-w-5xl px-4 md:px-6">
<ul className="font-sm mt-6 flex flex-row flex-wrap gap-4 text-neutral-600 dark:text-neutral-300">
<li> <li>
<a <a
className="flex items-center transition-all hover:text-neutral-800 dark:hover:text-neutral-100" className="flex items-center transition-all hover:text-neutral-800 dark:hover:text-neutral-100"
@ -42,9 +43,10 @@ export default function Footer() {
</a> </a>
</li> </li>
</ul> </ul>
<p className="mt-8 text-neutral-600 dark:text-neutral-300"> <p className="mt-6 text-neutral-600 dark:text-neutral-300">
© {new Date().getFullYear()} MIT Licensed © {new Date().getFullYear()} MIT Licensed
</p> </p>
</div>
</footer> </footer>
) )
} }

View File

@ -1,40 +1,77 @@
import Link from 'next/link' 'use client'
const navItems = { import Link from 'next/link'
'/': { import { usePathname } from 'next/navigation'
name: 'home', import { cn } from '@/lib/utils'
},
'/blog': { type NavItem = { href: string; label: string }
name: 'blog',
}, const mainNav: NavItem[] = [
'https://vercel.com/templates/next.js/portfolio-starter-kit': { { href: '/', label: 'Home' },
name: 'deploy', { href: '/blog', label: 'Blog' },
}, { href: '/#about', label: 'About' },
{ href: '/#contact', label: 'Contact' },
]
function NavLink({ href, label }: NavItem) {
const pathname = usePathname()
const isActive =
href === '/'
? pathname === '/'
: pathname.startsWith(href.replace('/#', '/'))
return (
<Link
href={href}
className={cn(
'px-2 py-1 text-sm rounded-md transition-colors',
'hover:bg-black/[0.04] dark:hover:bg-white/5',
isActive && 'text-foreground underline underline-offset-4'
)}
aria-current={isActive ? 'page' : undefined}
>
{label}
</Link>
)
} }
export function Navbar() { export function Navbar() {
return ( return (
<aside className="-ml-[8px] mb-16 tracking-tight"> <header className="sticky top-0 z-40 w-full border-b border-black/5 dark:border-white/10 bg-background/70 backdrop-blur">
<div className="lg:sticky lg:top-20"> <div className="mx-auto max-w-5xl px-4 md:px-6 h-14 flex items-center justify-between">
<nav <div className="font-semibold tracking-tight">
className="flex flex-row items-start relative px-0 pb-0 fade md:overflow-auto scroll-pr-6 md:relative" <Link
id="nav" href="/"
> className="px-2 py-1 rounded-md hover:bg-black/[0.04] dark:hover:bg-white/5"
<div className="flex flex-row space-x-0 pr-10"> aria-label="Home"
{Object.entries(navItems).map(([path, { name }]) => { >
return ( N
<Link </Link>
key={path} </div>
href={path}
className="transition-all hover:text-neutral-800 dark:hover:text-neutral-200 flex align-middle relative py-1 px-2 m-1" <nav className="flex items-center gap-1" aria-label="Primary">
> {mainNav.map((item) => (
{name} <NavLink key={item.href} {...item} />
</Link> ))}
)
})}
</div>
</nav> </nav>
<div className="flex items-center gap-2" aria-label="Utilities">
<Link
href="/rss"
className="px-2 py-1 text-sm rounded-md hover:bg-black/[0.04] dark:hover:bg-white/5"
>
RSS
</Link>
<a
href="https://github.com/vercel/next.js"
target="_blank"
rel="noreferrer"
className="px-2 py-1 text-sm rounded-md hover:bg-black/[0.04] dark:hover:bg-white/5"
>
GitHub
</a>
</div>
</div> </div>
</aside> </header>
) )
} }

View File

@ -0,0 +1,82 @@
'use client'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
import { useScrollContext } from '@/app/providers/LenisProvider'
type Item = { href: string; label: string }
const items: Item[] = [
{ href: '#about', label: 'About' },
{ href: '#writing', label: 'Writing' },
{ href: '#contact', label: 'Contact' },
]
export default function SidebarMenu() {
const [active, setActive] = useState<string>('')
const { lenis } = useScrollContext()
useEffect(() => {
const observers: IntersectionObserver[] = []
const onIntersect: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActive(`#${entry.target.id}`)
}
})
}
const opts: IntersectionObserverInit = { rootMargin: '-30% 0px -60% 0px', threshold: 0.1 }
const observer = new IntersectionObserver(onIntersect, opts)
items.forEach((it) => {
const id = it.href.replace('#', '')
const el = document.getElementById(id)
if (el) observer.observe(el)
})
observers.push(observer)
return () => {
observers.forEach((o) => o.disconnect())
}
}, [])
return (
<aside className="sticky top-24 h-fit">
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500">
Menu
</p>
<nav aria-label="Section menu" className="flex flex-col gap-1">
{items.map((it) => {
const isActive = active === it.href
return (
<Link
key={it.href}
href={`/${it.href}`}
onClick={(e) => {
const id = it.href.replace('#', '')
const el = document.getElementById(id)
if (el && lenis) {
e.preventDefault()
lenis.scrollTo(el, { offset: -80 })
try { history.replaceState(null, '', `/${it.href}`) } catch {}
}
}}
className={cn(
'px-3 py-2 text-sm rounded-md transition-colors',
'hover:bg-black/[0.04] dark:hover:bg-white/5',
isActive && 'bg-black/[0.06] dark:bg-white/10'
)}
aria-current={isActive ? 'true' : undefined}
>
{it.label}
</Link>
)
})}
</nav>
</aside>
)
}

View File

@ -4,6 +4,8 @@ import "./globals.css";
import { DotBackground } from "@/app/components/dotbackground"; import { DotBackground } from "@/app/components/dotbackground";
import { Navbar } from './components/nav' import { Navbar } from './components/nav'
import Footer from './components/footer' import Footer from './components/footer'
import { LenisProvider } from "./providers/LenisProvider";
import { MotionConfigProvider } from "./providers/MotionConfigProvider";
import { baseUrl } from './sitemap'; import { baseUrl } from './sitemap';
const geistSans = Geist({ const geistSans = Geist({
@ -59,12 +61,16 @@ export default function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<Navbar /> <MotionConfigProvider>
<DotBackground /> <LenisProvider>
<main className="flex-auto min-w-0 mt-6 flex flex-col px-2 md:px-0"> <Navbar />
{children} <DotBackground />
<Footer /> <main className="flex-auto min-w-0 mt-6 flex flex-col px-2 md:px-0">
</main> {children}
<Footer />
</main>
</LenisProvider>
</MotionConfigProvider>
</body> </body>
</html> </html>
); );

View File

@ -1,4 +1,7 @@
import React from "react" import React from "react"
import Link from "next/link"
import SidebarMenu from "@/app/components/sidebar-menu"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { FlipWords } from "@/components/ui/flip-words" import { FlipWords } from "@/components/ui/flip-words"
import { AvatarMotion } from "@/app/components/avatar-motion" import { AvatarMotion } from "@/app/components/avatar-motion"
import { BlogPosts } from "@/components/posts" import { BlogPosts } from "@/components/posts"
@ -21,100 +24,132 @@ export default function Home() {
const year = new Date().getFullYear(); const year = new Date().getFullYear();
return ( return (
<main className="flex min-h-screen items-center justify-center antialiased"> <main className="min-h-screen">
<div className="w-full max-w-xl mx-auto flex flex-col items-center text-center gap-8 px-4 py-12"> <div className="mx-auto grid w-full max-w-5xl grid-cols-1 gap-6 px-4 pt-10 pb-16 md:grid-cols-12 md:gap-8">
<section aria-label="Profile photo"> {/* Sidebar */}
<AvatarMotion <div className="md:col-span-3">
src="/images/profile.jpg" <SidebarMenu />
alt="Hand drawn portrait of Nicholai" </div>
size={160}
/>
</section>
<section aria-labelledby="intro-title" className="space-y-2"> {/* Content column */}
<h1 id="intro-title" className="text-2xl font-semibold"> <div className="md:col-span-9 flex flex-col gap-6">
Nicholai {/* About */}
</h1> <section id="about" aria-label="About">
<p className="text-xs text-neutral-600 dark:text-neutral-400 font-normal"> <Card>
I wanted to justify the $6.39 I spent on the domain. <CardHeader className="flex items-center gap-4 sm:flex-row">
</p> <AvatarMotion
<div className="text-sm text-neutral-700 dark:text-neutral-400"> src="/images/profile.jpg"
<FlipWords words={["VFX Artist", "Developer"]} className="text-neutral-900 dark:text-neutral-200" /> alt="Hand drawn portrait of Nicholai"
</div> size={96}
</section> />
<div>
<CardTitle className="tracking-tight !normal-case">About</CardTitle>
<CardDescription>Nicholai VFX Artist & Developer</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-neutral-700 dark:text-neutral-300">
I wanted to justify the $6.39 I spent on the domain. Building cinematic, accessible web experiences.
</p>
<nav aria-label="Hyperlinks" className="w-full space-y-1"> <div className="text-sm text-neutral-800 dark:text-neutral-200">
<p className="text-xs font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500"> <FlipWords words={["VFX Artist", "Developer"]} className="font-medium" />
Hyperlinks </div>
</p>
<ul className="space-y-1">
<li className="text-neutral-900 dark:text-neutral-200">
VFX Supervisor at{" "}
<a
href="https://biohazardvfx.com"
className=""
target="_blank"
rel="noopener noreferrer"
>
Biohazard VFX
</a>
</li>
<li className="text-neutral-900 dark:text-neutral-200">
Developer{" "}
<a
href="https://fortura.cc"
className=""
target="_blank"
rel="noopener noreferrer"
>
Fortura Data Solutions
</a>
</li>
<li>
<a
href="mailto:nicholai@biohazardvfx.com"
className="text-xs"
>
Email me
</a>
</li>
<li className="text-neutral-900 dark:text-neutral-200">
<a
href="https://www.instagram.com/nicholai.exe/"
className=""
target="_blank"
rel="me noopener noreferrer"
>
Instagram
</a>
<span className="text-xs text-neutral-500"> - I hate Instagram</span>
</li>
</ul>
</nav>
<div className="my-8">
<BlogPosts />
</div>
<section aria-labelledby="listening-title" className="w-full space-y-4"> <ul className="flex flex-wrap gap-3 pt-1 text-sm">
<h3 id="listening-title" className="text-sm font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500 mb-4"> <li className="text-neutral-900 dark:text-neutral-200">
Listening VFX Supervisor at{" "}
</h3> <a href="https://biohazardvfx.com" target="_blank" rel="noopener noreferrer">
<div className="flex-1 flex items-center justify-center"> Biohazard VFX
<iframe </a>
title="Spotify playlist" </li>
style={{ borderRadius: 12 }} <li className="text-neutral-900 dark:text-neutral-200">
src="https://open.spotify.com/embed/playlist/1kV9JPnhvpfk0UtcpslRpa?utm_source=generator&theme=0" Developer{" "}
width="290" <a href="https://fortura.cc" target="_blank" rel="noopener noreferrer">
height="220" Fortura Data Solutions
loading="lazy" </a>
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" </li>
/> <li>
</div> <a href="mailto:nicholai@biohazardvfx.com" className="underline underline-offset-4">
</section> Email me
</a>
</li>
<li className="text-neutral-900 dark:text-neutral-200">
<a
href="https://www.instagram.com/nicholai.exe/"
target="_blank"
rel="me noopener noreferrer"
>
Instagram
</a>
<span className="ml-1 text-xs text-neutral-500"> I hate Instagram</span>
</li>
</ul>
<footer className="pt-6 text-center text-xs text-neutral-600 dark:text-neutral-500"> {/* Listening */}
© {year} Nicholai · $6.39 well spent. <div className="pt-2">
</footer> <h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500">
Listening
</h3>
<div className="spotify-card">
<iframe
title="Spotify playlist"
style={{ borderRadius: 12 }}
src="https://open.spotify.com/embed/playlist/1kV9JPnhvpfk0UtcpslRpa?utm_source=generator&theme=0"
width="290"
height="220"
loading="lazy"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
/>
</div>
</div>
</CardContent>
</Card>
</section>
{/* Writing */}
<section id="writing" aria-label="Writing">
<Card>
<CardHeader>
<CardTitle className="!normal-case">Writing</CardTitle>
<CardDescription>Recent posts</CardDescription>
</CardHeader>
<CardContent>
<div className="my-2">
<BlogPosts />
</div>
<div className="mt-4">
<Link
href="/blog"
className="inline-flex items-center rounded-md border border-black/10 dark:border-white/10 px-3 py-2 text-sm hover:bg-black/[0.04] dark:hover:bg-white/5"
>
View all posts
</Link>
</div>
</CardContent>
</Card>
</section>
{/* Contact */}
<section id="contact" aria-label="Contact">
<Card>
<CardHeader>
<CardTitle className="!normal-case">Contact</CardTitle>
<CardDescription>Let's build something</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm">
Prefer email:
{" "}
<a href="mailto:nicholai@biohazardvfx.com" className="underline underline-offset-4">
nicholai@biohazardvfx.com
</a>
</p>
<p className="footer-small mt-4">© {year} Nicholai · $6.39 well spent.</p>
</CardContent>
</Card>
</section>
</div>
</div> </div>
</main> </main>
) )

View File

@ -39,16 +39,17 @@ export function LenisProvider({ children }: { children: React.ReactNode }) {
window.matchMedia("(prefers-reduced-motion: reduce)").matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const lenis = new Lenis({ const lenis = new Lenis({
// Good defaults for premium-feel scroll // Softer, less "floaty" feel
smoothWheel: !prefersReducedMotion, smoothWheel: !prefersReducedMotion,
syncTouch: true, syncTouch: true,
duration: 1.2, // seconds to ease to target position duration: 0.7, // was 1.2
easing: (t: number) => 1 - Math.pow(1 - t, 3), // easeOutCubic easing: (t: number) => 1 - Math.pow(1 - t, 2.4), // gentler ease-out
wheelMultiplier: 0.9, // slightly reduce wheel amplitude
// Use native on reduced motion // Use native on reduced motion
wrapper: undefined, wrapper: undefined,
content: undefined, content: undefined,
// If reduced motion, disable smoothing entirely // If reduced motion, disable smoothing entirely
lerp: prefersReducedMotion ? 1 : 0.1, lerp: prefersReducedMotion ? 1 : 0.22, // was 0.1 (heavier smoothing)
}); });
lenisRef.current = lenis; lenisRef.current = lenis;

View File

@ -5,32 +5,33 @@ export function BlogPosts() {
const allBlogs = getBlogPosts() const allBlogs = getBlogPosts()
return ( return (
<div> <ul className="divide-y divide-black/5 dark:divide-white/10">
{allBlogs {allBlogs
.sort((a, b) => { .sort((a, b) => {
if ( if (new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)) {
new Date(a.metadata.publishedAt) > new Date(b.metadata.publishedAt)
) {
return -1 return -1
} }
return 1 return 1
}) })
.map((post) => ( .map((post) => (
<Link <li key={post.slug}>
key={post.slug} <Link
className="flex flex-col space-y-1 mb-4" className="group flex items-baseline justify-between gap-4 rounded-md px-2 py-2 transition-colors hover:bg-black/[0.04] dark:hover:bg-white/5"
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
> aria-label={`Read: ${post.metadata.title}`}
<div className="w-full flex flex-col md:flex-row space-x-0 md:space-x-2"> >
<p className="text-neutral-600 dark:text-neutral-400 w-[100px] tabular-nums"> <span className="w-32 flex-none text-xs text-neutral-600 dark:text-neutral-400 tabular-nums">
{formatDate(post.metadata.publishedAt, false)} {formatDate(post.metadata.publishedAt, false)}
</p> </span>
<p className="text-neutral-900 dark:text-neutral-100 tracking-tight"> <span className="flex-1 text-neutral-900 dark:text-neutral-100 tracking-tight underline-offset-4 group-hover:underline">
{post.metadata.title} {post.metadata.title}
</p> </span>
</div> <span className="hidden sm:inline text-neutral-400 transition-colors group-hover:text-neutral-600 dark:group-hover:text-neutral-300">
</Link>
</span>
</Link>
</li>
))} ))}
</div> </ul>
) )
} }

61
components/ui/card.tsx Normal file
View File

@ -0,0 +1,61 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export function Card({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'rounded-lg border border-black/10 dark:border-white/10',
'bg-white/60 dark:bg-white/[0.03] shadow-sm backdrop-blur-sm',
className
)}
{...props}
/>
)
}
export function CardHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('px-5 py-4', className)} {...props} />
}
export function CardTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3
className={cn(
'text-sm font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-400',
className
)}
{...props}
/>
)
}
export function CardDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn('text-sm text-neutral-600 dark:text-neutral-400', className)}
{...props}
/>
)
}
export function CardContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('px-5 pb-5', className)} {...props} />
}

36
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lenis": "^1.3.11",
"mdx": "^0.3.1", "mdx": "^0.3.1",
"motion": "^12.23.12", "motion": "^12.23.12",
"next": "15.5.2", "next": "15.5.2",
@ -1425,6 +1426,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -1491,6 +1493,7 @@
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.43.0", "@typescript-eslint/types": "8.43.0",
@ -2013,6 +2016,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3108,6 +3112,7 @@
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -3282,6 +3287,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -4840,6 +4846,32 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/lenis": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.11.tgz",
"integrity": "sha512-lkyBnNTVwJzlupp+VL6LTn62WeT8WponuLpmTU0Z20cMwMsLLjqbSqwuA7I1yKSVWCBj/awo4jnFzOMOVCB8OQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/darkroomengineering"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"react": ">=17.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"react": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -6773,6 +6805,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -6782,6 +6815,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -7816,6 +7850,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7994,6 +8029,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lenis": "^1.3.11",
"mdx": "^0.3.1", "mdx": "^0.3.1",
"motion": "^12.23.12", "motion": "^12.23.12",
"next": "15.5.2", "next": "15.5.2",