From 4aa27ff2d1185d76859c6de76f1aad314d69f742 Mon Sep 17 00:00:00 2001 From: Jet Joseph Date: Sun, 3 May 2026 13:33:15 +0800 Subject: [PATCH 1/2] v1.1.0 --- package.json | 2 +- src/App.tsx | 11 +- src/components/About/About.module.css | 21 +--- src/components/About/About.tsx | 9 +- src/components/Hero/Hero.tsx | 9 +- src/components/Projects/Projects.module.css | 19 +--- src/components/Projects/Projects.tsx | 9 +- src/components/Skills/Skills.module.css | 21 +--- src/components/Skills/Skills.tsx | 9 +- src/lib/motion.ts | 15 +++ src/styles/index.css | 118 +++++++++++++------- tests/e2e/app.spec.ts | 6 +- tests/integration/theme.test.tsx | 20 ++-- 13 files changed, 124 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index d6923ff..6605914 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "themockingjet.github.io", "private": true, - "version": "1.0.0", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index 2d1c478..37156ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,8 @@ const Skills = lazy(() => import('./components/Skills/Skills')) const portfolio = parsePortfolio(rawPortfolio) export const THEMES = [ - { id: 'warm', label: 'Warm', accent: '#5e8a70' }, + { id: 'amber', label: 'Amber', accent: '#f59e0b' }, + { id: 'sage', label: 'Sage', accent: '#5e8a70' }, { id: 'ocean', label: 'Ocean', accent: '#4a9eff' }, { id: 'ember', label: 'Ember', accent: '#e07840' }, { id: 'violet', label: 'Violet', accent: '#8b67e8' }, @@ -31,16 +32,16 @@ const REVEAL = { export default function App() { const [dark, setDark] = useState(true) - const [theme, setTheme] = useState('warm') + const [theme, setTheme] = useState('amber') const toggleDark = () => setDark((d) => !d) useEffect(() => { - if (dark) document.documentElement.removeAttribute('data-dark') - else document.documentElement.setAttribute('data-dark', '0') + if (dark) document.documentElement.removeAttribute('data-mode') + else document.documentElement.setAttribute('data-mode', 'light') }, [dark]) useEffect(() => { - if (theme === 'warm') document.documentElement.removeAttribute('data-theme') + if (theme === 'amber') document.documentElement.removeAttribute('data-theme') else document.documentElement.setAttribute('data-theme', theme) }, [theme]) diff --git a/src/components/About/About.module.css b/src/components/About/About.module.css index 628bd28..6d56e1e 100644 --- a/src/components/About/About.module.css +++ b/src/components/About/About.module.css @@ -4,7 +4,7 @@ padding: var(--space-16) 0; } -/* Ambient glow — centered on watermark text */ +/* Ambient glow */ .about::after { content: ''; position: absolute; @@ -21,26 +21,9 @@ /* Background watermark */ .decor { - position: absolute; + composes: section-decor from global; left: 5%; top: 30%; - font-size: 22vw; - font-weight: 800; - line-height: 1; - white-space: nowrap; - background: radial-gradient(ellipse at center, - color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, - color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, - color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, - transparent 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - pointer-events: none; - user-select: none; - letter-spacing: -0.04em; - opacity: 0.5; } .inner { diff --git a/src/components/About/About.tsx b/src/components/About/About.tsx index 5cf16ba..825f371 100644 --- a/src/components/About/About.tsx +++ b/src/components/About/About.tsx @@ -2,7 +2,7 @@ import { motion, AnimatePresence, useScroll, useTransform } from 'motion/react' import { useEffect, useState, useRef } from 'react' import styles from './About.module.css' import type { AboutData } from '../../lib/parsePortfolio' -import { EASE, STAGGER } from '../../lib/motion' +import { EASE, STAGGER, FADE_UP } from '../../lib/motion' interface AboutProps { data: AboutData @@ -13,11 +13,6 @@ const FADE_MS = 0.35 const ENTER = STAGGER -const SLOT_ENTER = { - hidden: { opacity: 0, y: 12 }, - show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } }, -} - function WordSlot({ variants, personaIdx }: { variants: string[]; personaIdx: number }) { const [hasSwapped, setHasSwapped] = useState(false) const prevIdx = useRef(personaIdx) @@ -128,7 +123,7 @@ export default function About({ data }: AboutProps) { viewport={{ once: true, margin: '-60px' }} > {slots.map(({ label, segments }, i) => ( - + // {label}

{segments.map((seg, j) => diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index 90a9487..fdf7873 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -2,17 +2,12 @@ import { motion, useScroll, useTransform } from 'motion/react' import { useEffect, useRef, useState } from 'react' import styles from './Hero.module.css' import type { HeroData } from '../../lib/parsePortfolio' -import { EASE } from '../../lib/motion' +import { EASE, STAGGER_DELAY } from '../../lib/motion' interface HeroProps { data: HeroData } -const container = { - hidden: {}, - show: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } }, -} - const item = { hidden: { opacity: 0, y: 18 }, show: { opacity: 1, y: 0, transition: { duration: 0.65, ease: EASE } }, @@ -90,7 +85,7 @@ export default function Hero({ data }: HeroProps) { diff --git a/src/components/Projects/Projects.module.css b/src/components/Projects/Projects.module.css index d352509..bb54eee 100644 --- a/src/components/Projects/Projects.module.css +++ b/src/components/Projects/Projects.module.css @@ -21,26 +21,9 @@ /* Background watermark */ .decor { - position: absolute; + composes: section-decor from global; left: 5%; top: 50%; - font-size: 22vw; - font-weight: 800; - line-height: 1; - white-space: nowrap; - background: radial-gradient(ellipse at center, - color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, - color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, - color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, - transparent 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - pointer-events: none; - user-select: none; - letter-spacing: -0.04em; - opacity: 0.5; } .inner { diff --git a/src/components/Projects/Projects.tsx b/src/components/Projects/Projects.tsx index 63019e1..e70006a 100644 --- a/src/components/Projects/Projects.tsx +++ b/src/components/Projects/Projects.tsx @@ -4,7 +4,7 @@ import { Icon } from '@iconify/react' import styles from './Projects.module.css' import type { ProjectData } from '../../lib/parsePortfolio' import { resolveIcon } from '../../lib/icons' -import { EASE, STAGGER } from '../../lib/motion' +import { EASE, STAGGER, FADE_UP } from '../../lib/motion' const STATUS_CONFIG: Record = { active: { label: 'Active', color: 'var(--status-active)' }, @@ -16,11 +16,6 @@ function resolveStatus(status: string) { return STATUS_CONFIG[status] ?? { label: status, color: 'var(--text-muted)' } } -const CARD = { - hidden: { opacity: 0, y: 18 }, - show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } }, -} - interface ProjectsProps { @@ -67,7 +62,7 @@ function ProjectCard({ project, index, featured }: { project: ProjectData; index ].filter(Boolean).join(' ') return ( - + {indexLabel} diff --git a/src/components/Skills/Skills.module.css b/src/components/Skills/Skills.module.css index a008a9b..3c42f61 100644 --- a/src/components/Skills/Skills.module.css +++ b/src/components/Skills/Skills.module.css @@ -21,28 +21,9 @@ /* Background watermark */ .decor { - position: absolute; + composes: section-decor from global; left: 10%; top: 40%; - font-size: 22vw; - font-weight: 800; - line-height: 1; - white-space: nowrap; - background: radial-gradient( - ellipse at center, - color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, - color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, - color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, - transparent 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; - pointer-events: none; - user-select: none; - letter-spacing: -0.04em; - opacity: 0.5; } .inner { diff --git a/src/components/Skills/Skills.tsx b/src/components/Skills/Skills.tsx index f8f6feb..13e1194 100644 --- a/src/components/Skills/Skills.tsx +++ b/src/components/Skills/Skills.tsx @@ -4,18 +4,13 @@ import { Icon } from '@iconify/react' import styles from './Skills.module.css' import type { SkillsData } from '../../lib/parsePortfolio' import { resolveIcon } from '../../lib/icons' -import { EASE, STAGGER } from '../../lib/motion' +import { EASE, STAGGER, FADE_UP_SM } from '../../lib/motion' const CHIP = { hidden: { opacity: 0, scale: 0.92 }, show: { opacity: 1, scale: 1, transition: { duration: 0.4, ease: EASE } }, } -const ROW = { - hidden: { opacity: 0, y: 10 }, - show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: EASE } }, -} - interface SkillsProps { skills: SkillsData } @@ -23,7 +18,7 @@ interface SkillsProps { function SkillGroup({ label, items }: { label: string; items: string[] }) { if (items.length === 0) return null return ( - +

{label}

{items.map((item) => { diff --git a/src/lib/motion.ts b/src/lib/motion.ts index 01339cc..c04e2e0 100644 --- a/src/lib/motion.ts +++ b/src/lib/motion.ts @@ -4,3 +4,18 @@ export const STAGGER = { hidden: {}, show: { transition: { staggerChildren: 0.08 } }, } + +export const STAGGER_DELAY = { + hidden: {}, + show: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } }, +} + +export const FADE_UP = { + hidden: { opacity: 0, y: 18 }, + show: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } }, +} + +export const FADE_UP_SM = { + hidden: { opacity: 0, y: 10 }, + show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: EASE } }, +} diff --git a/src/styles/index.css b/src/styles/index.css index 6d50af1..63560fe 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -47,28 +47,28 @@ button { } :root { - /* Warm dark */ - --color-bg: #0d0c0b; - --color-bg-surface: #141210; - --color-bg-elevated: #1b1815; - --color-bg-hover: #22201c; - --color-border: rgba(255, 255, 255, 0.07); - --color-border-focus: rgba(94, 138, 112, 0.4); - - /* Sage green accent */ - --color-accent: #5e8a70; - --color-accent-dim: rgba(94, 138, 112, 0.1); - --color-accent-glow: rgba(94, 138, 112, 0.22); - - /* Warm off-white text */ - --color-text: #dbd5cc; - --color-text-muted: #918780; - --color-text-faint: #8a8078; + /* Slate dark */ + --color-bg: #0f1219; + --color-bg-surface: #151a24; + --color-bg-elevated: #1a1f2e; + --color-bg-hover: #222838; + --color-border: rgba(255, 255, 255, 0.08); + --color-border-focus: rgba(245, 158, 11, 0.4); + + /* Amber accent */ + --color-accent: #f59e0b; + --color-accent-dim: rgba(245, 158, 11, 0.1); + --color-accent-glow: rgba(245, 158, 11, 0.22); + + /* Cool off-white text */ + --color-text: #e2e0dc; + --color-text-muted: #9ba0aa; + --color-text-faint: #7f8594; /* Status */ - --status-active: #6aad84; - --status-beta: #c4924f; - --status-deprecated: #4a4440; + --status-active: #34d399; + --status-beta: #f59e0b; + --status-deprecated: #4a4e5a; /* Layout */ --container-max: 780px; @@ -135,6 +135,15 @@ button { --status-active: #a080f0; } +/* Theme: Sage */ +:root[data-theme="sage"] { + --color-accent: #5e8a70; + --color-accent-dim: rgba(94, 138, 112, 0.1); + --color-accent-glow: rgba(94, 138, 112, 0.22); + --color-border-focus: rgba(94, 138, 112, 0.4); + --status-active: #6aad84; +} + /* Theme: Slate */ :root[data-theme="slate"] { --color-accent: #5b8fa8; @@ -145,51 +154,56 @@ button { } /* Light mode */ -:root[data-dark="0"] { - --color-bg: #faf9f7; - --color-bg-surface: #f0eee9; +:root[data-mode="light"] { + --color-bg: #f8f9fb; + --color-bg-surface: #eef0f4; --color-bg-elevated: #ffffff; - --color-bg-hover: #eceae5; + --color-bg-hover: #e4e7ed; --color-border: rgba(0, 0, 0, 0.12); - --color-text: #1a1714; - --color-text-muted: #7a7068; - --color-text-faint: #767068; - --status-deprecated: #c8c0b5; - --topbar-bg: rgba(250, 249, 247, 0.92); + --color-text: #141820; + --color-text-muted: #5c6370; + --color-text-faint: #7a8090; + --status-deprecated: #b8bcc5; + --topbar-bg: rgba(248, 249, 251, 0.92); } -/* Boost glow in light mode (accent-glow is too subtle on bright backgrounds) */ -/* Also darken accents to pass WCAG AA (4.5:1) on light background */ -:root[data-dark="0"] { - --color-accent: #3d6b50; - --color-accent-dim: rgba(61, 107, 80, 0.1); - --color-accent-glow: rgba(61, 107, 80, 0.38); - --color-border-focus: rgba(61, 107, 80, 0.4); +/* Darken accents to pass WCAG AA (4.5:1) on light background */ +:root[data-mode="light"] { + --color-accent: #b37400; + --color-accent-dim: rgba(179, 116, 0, 0.1); + --color-accent-glow: rgba(179, 116, 0, 0.35); + --color-border-focus: rgba(179, 116, 0, 0.4); } -:root[data-dark="0"][data-theme="ocean"] { +:root[data-mode="light"][data-theme="ocean"] { --color-accent: #1a6fd4; --color-accent-dim: rgba(26, 111, 212, 0.1); --color-accent-glow: rgba(26, 111, 212, 0.35); --color-border-focus: rgba(26, 111, 212, 0.4); } -:root[data-dark="0"][data-theme="ember"] { +:root[data-mode="light"][data-theme="ember"] { --color-accent: #a8501a; --color-accent-dim: rgba(168, 80, 26, 0.1); --color-accent-glow: rgba(168, 80, 26, 0.35); --color-border-focus: rgba(168, 80, 26, 0.4); } -:root[data-dark="0"][data-theme="violet"] { +:root[data-mode="light"][data-theme="violet"] { --color-accent: #6840c0; --color-accent-dim: rgba(104, 64, 192, 0.1); --color-accent-glow: rgba(104, 64, 192, 0.35); --color-border-focus: rgba(104, 64, 192, 0.4); } -:root[data-dark="0"][data-theme="slate"] { +:root[data-mode="light"][data-theme="slate"] { --color-accent: #3a6e88; --color-accent-dim: rgba(58, 110, 136, 0.1); --color-accent-glow: rgba(58, 110, 136, 0.35); --color-border-focus: rgba(58, 110, 136, 0.4); } +:root[data-mode="light"][data-theme="sage"] { + --color-accent: #3d6b50; + --color-accent-dim: rgba(61, 107, 80, 0.1); + --color-accent-glow: rgba(61, 107, 80, 0.35); + --color-border-focus: rgba(61, 107, 80, 0.4); +} /* Scale up type for large screens */ @media (min-width: 1200px) { @@ -241,7 +255,7 @@ body::before { #root { position: relative; z-index: 1; } /* Reduce texture in light mode */ -[data-dark="0"] body::before { +[data-mode="light"] body::before { opacity: 0.25; background-image: url('/textures/classy-fabric.png'), @@ -277,6 +291,28 @@ body::before { background: var(--color-border); } +/* Shared watermark / decor text */ +.section-decor { + position: absolute; + font-size: 22vw; + font-weight: 800; + line-height: 1; + white-space: nowrap; + background: radial-gradient(ellipse at center, + color-mix(in srgb, var(--color-accent) 35%, transparent) 0%, + color-mix(in srgb, var(--color-accent) 20%, transparent) 40%, + color-mix(in srgb, var(--color-accent) 8%, transparent) 70%, + transparent 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + pointer-events: none; + user-select: none; + letter-spacing: -0.04em; + opacity: 0.5; +} + .status-dot { display: inline-flex; align-items: center; diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts index f262445..736fad7 100644 --- a/tests/e2e/app.spec.ts +++ b/tests/e2e/app.spec.ts @@ -39,11 +39,11 @@ test.describe('Critical user flows', () => { await page.goto('/') // Toggle to light await page.getByRole('button', { name: /switch to light/i }).click() - const dark = await page.locator('html').getAttribute('data-dark') - expect(dark).toBe('0') + const dark = await page.locator('html').getAttribute('data-mode') + expect(dark).toBe('light') // Toggle back to dark await page.getByRole('button', { name: /switch to dark/i }).click() - const darkAfter = await page.locator('html').getAttribute('data-dark') + const darkAfter = await page.locator('html').getAttribute('data-mode') expect(darkAfter).toBeNull() }) diff --git a/tests/integration/theme.test.tsx b/tests/integration/theme.test.tsx index d5f53f9..c41f44d 100644 --- a/tests/integration/theme.test.tsx +++ b/tests/integration/theme.test.tsx @@ -5,11 +5,11 @@ import App from '../../src/App' describe('Theme switching', () => { beforeEach(() => { document.documentElement.removeAttribute('data-theme') - document.documentElement.removeAttribute('data-dark') + document.documentElement.removeAttribute('data-mode') }) describe('given the default state', () => { - it('should start with the warm theme (no data-theme attribute)', () => { + it('should start with the amber theme (no data-theme attribute)', () => { render() expect(document.documentElement.getAttribute('data-theme')).toBeNull() }) @@ -24,14 +24,14 @@ describe('Theme switching', () => { expect(document.documentElement.getAttribute('data-theme')).toBe('ocean') }) - it('should switch back to warm when selected again', async () => { + it('should switch back to amber when selected again', async () => { const user = userEvent.setup() render() const themeBtn = screen.getByRole('button', { name: /theme/i }) await user.click(themeBtn) await user.click(screen.getByRole('option', { name: /ocean/i })) await user.click(themeBtn) - await user.click(screen.getByRole('option', { name: /warm/i })) + await user.click(screen.getByRole('option', { name: /amber/i })) expect(document.documentElement.getAttribute('data-theme')).toBeNull() }) @@ -48,22 +48,22 @@ describe('Theme switching', () => { describe('Dark mode toggle', () => { beforeEach(() => { - document.documentElement.removeAttribute('data-dark') + document.documentElement.removeAttribute('data-mode') }) describe('given the default dark mode', () => { - it('should start without a data-dark attribute', () => { + it('should start without a data-mode attribute', () => { render() - expect(document.documentElement.getAttribute('data-dark')).toBeNull() + expect(document.documentElement.getAttribute('data-mode')).toBeNull() }) }) describe('when the user toggles to light mode', () => { - it('should set data-dark="0" on the document', async () => { + it('should set data-mode="light" on the document', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: /switch to light/i })) - expect(document.documentElement.getAttribute('data-dark')).toBe('0') + expect(document.documentElement.getAttribute('data-mode')).toBe('light') }) it('should toggle back to dark mode on second click', async () => { @@ -71,7 +71,7 @@ describe('Dark mode toggle', () => { render() await user.click(screen.getByRole('button', { name: /switch to light/i })) await user.click(screen.getByRole('button', { name: /switch to dark/i })) - expect(document.documentElement.getAttribute('data-dark')).toBeNull() + expect(document.documentElement.getAttribute('data-mode')).toBeNull() }) }) }) From ac7dddd49688b24681e65d3be4039af5f5a615b0 Mon Sep 17 00:00:00 2001 From: Jet Joseph Date: Sun, 3 May 2026 15:24:19 +0800 Subject: [PATCH 2/2] v1.2.0 --- package.json | 2 +- src/components/About/About.module.css | 43 +++---------- src/components/About/About.tsx | 11 +--- src/components/Projects/Projects.module.css | 70 +++++++++------------ src/components/Projects/Projects.tsx | 11 +--- src/components/Skills/Skills.module.css | 29 +++------ src/components/Skills/Skills.tsx | 11 +--- src/styles/index.css | 26 +++----- 8 files changed, 58 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index 6605914..da8ebc0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "themockingjet.github.io", "private": true, - "version": "1.1.0", + "version": "1.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/About/About.module.css b/src/components/About/About.module.css index 6d56e1e..83db1d1 100644 --- a/src/components/About/About.module.css +++ b/src/components/About/About.module.css @@ -1,29 +1,21 @@ /* Section container */ .about { + composes: section-grid from global; position: relative; padding: var(--space-16) 0; + overflow: visible; } -/* Ambient glow */ .about::after { content: ''; position: absolute; - left: 5%; - top: 30%; - width: 60vw; - height: 60vw; - transform: translate(-20%, -50%); - background: radial-gradient(circle, var(--color-accent-glow) 0%, transparent 60%); + left: -10vw; + bottom: -8vw; + width: 45vw; + height: 45vw; + background: radial-gradient(ellipse at center, color-mix(in srgb, var(--color-accent) 10%, transparent) 0%, transparent 65%); pointer-events: none; z-index: 0; - opacity: 0.35; -} - -/* Background watermark */ -.decor { - composes: section-decor from global; - left: 5%; - top: 30%; } .inner { @@ -54,17 +46,6 @@ .prose { max-width: 58ch; position: relative; - padding-left: var(--space-6); -} - -.prose::before { - content: ''; - position: absolute; - left: 0; - top: 4px; - bottom: 4px; - width: 2px; - background: linear-gradient(to bottom, var(--color-accent), transparent); } /* Paragraph block (one per intent) */ @@ -109,16 +90,6 @@ border-bottom: 1px solid transparent; } -@media (max-width: 1024px) { - .decor { - display: none; - } - - .about::after { - display: none; - } -} - @media (max-width: 767px) { .about { padding: var(--space-12) 0; diff --git a/src/components/About/About.tsx b/src/components/About/About.tsx index 825f371..464ca96 100644 --- a/src/components/About/About.tsx +++ b/src/components/About/About.tsx @@ -1,4 +1,4 @@ -import { motion, AnimatePresence, useScroll, useTransform } from 'motion/react' +import { motion, AnimatePresence } from 'motion/react' import { useEffect, useState, useRef } from 'react' import styles from './About.module.css' import type { AboutData } from '../../lib/parsePortfolio' @@ -55,12 +55,6 @@ export default function About({ data }: AboutProps) { const [ready, setReady] = useState(false) const sectionRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [70, -110]) - useEffect(() => { const t = setTimeout(() => setReady(true), 1800) return () => clearTimeout(t) @@ -77,9 +71,6 @@ export default function About({ data }: AboutProps) { return (
-
p.status === 'deprecated') const sectionRef = useRef(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [80, -120]) - return (
-
(null) - const { scrollYProgress } = useScroll({ - target: sectionRef, - offset: ['start end', 'end start'], - }) - const decorY = useTransform(scrollYProgress, [0, 1], [60, -100]) - return (
-