From e37517dc3dfc5592e405cf05325b5c57609ac0b4 Mon Sep 17 00:00:00 2001 From: Abdullah <125115953+mabdullahabaid@users.noreply.github.com> Date: Wed, 20 May 2026 04:22:02 +0500 Subject: [PATCH] fix(website): polish product hero opacity sync and hydration Align dark overlay and nav opacity with the AI panel when fully in view, use stable tab ids and remove Edit icon clipPath useId to fix SSR mismatches, and stabilize menu scroll effect dependencies. --- .../src/icons/informative/Edit.tsx | 17 +----- .../ProductVisual/HeroVisualScroll.tsx | 58 +++++++++---------- .../ProductVisual/hero-scroll-colors.ts | 33 ++++++++--- .../ProductVisual/product-hero-menu-sync.tsx | 13 +++-- .../ProductVisual/product-hero-tabs-id.ts | 1 + 5 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-tabs-id.ts diff --git a/packages/twenty-website/src/icons/informative/Edit.tsx b/packages/twenty-website/src/icons/informative/Edit.tsx index 65957cb75b0..e3b00810b76 100644 --- a/packages/twenty-website/src/icons/informative/Edit.tsx +++ b/packages/twenty-website/src/icons/informative/Edit.tsx @@ -1,12 +1,6 @@ -'use client'; - -import { useId } from 'react'; - type EditIconProps = { size: number; color: string; strokeWidth?: number }; export function EditIcon({ size, color, strokeWidth = 2 }: EditIconProps) { - const clipPathId = useId().replace(/:/g, ''); - return ( - + - - - - - ); } diff --git a/packages/twenty-website/src/sections/Hero/components/ProductVisual/HeroVisualScroll.tsx b/packages/twenty-website/src/sections/Hero/components/ProductVisual/HeroVisualScroll.tsx index f02e9801cb0..ee82869a2be 100644 --- a/packages/twenty-website/src/sections/Hero/components/ProductVisual/HeroVisualScroll.tsx +++ b/packages/twenty-website/src/sections/Hero/components/ProductVisual/HeroVisualScroll.tsx @@ -2,8 +2,8 @@ import { type ReactNode, + useCallback, useEffect, - useId, useLayoutEffect, useRef, useState, @@ -24,6 +24,7 @@ import { getHeroScrollMotion, } from './hero-scroll-colors'; import { useProductHeroMenuSync } from './product-hero-menu-sync'; +import { PRODUCT_HERO_AI_TABS_ID } from './product-hero-tabs-id'; import { ProductVisual } from './ProductVisual'; import { useHeroScrollProgress } from './use-hero-scroll-progress'; @@ -215,8 +216,8 @@ const PanelVisual = styled.div` } `; -const INTRO_PANEL_COLORS = getHeroScrollColors(0); -const AI_PANEL_COLORS = getHeroScrollColors(1); +const INTRO_PANEL_COLORS = getHeroScrollColors(0, 0); +const AI_PANEL_COLORS = getHeroScrollColors(1, 1); export function HeroVisualScroll({ aiBody, @@ -236,9 +237,8 @@ export function HeroVisualScroll({ useHeroScrollProgress(trackRef); const [activeTab, setActiveTab] = useState(0); const [panelStepPixels, setPanelStepPixels] = useState(0); - const idPrefix = useId(); - const heroColors = getHeroScrollColors(colorMix); + const heroColors = getHeroScrollColors(colorMix, panelMix); const heroMotion = getHeroScrollMotion(panelMix, panelStepPixels); const introPanelColors = isScrollDriven ? heroColors : INTRO_PANEL_COLORS; const aiPanelColors = isScrollDriven ? heroColors : AI_PANEL_COLORS; @@ -264,38 +264,38 @@ export function HeroVisualScroll({ return () => resizeObserver.disconnect(); }, [aiBody, aiHeading, introBody, introHeading, isScrollDriven, tabs]); - useEffect(() => { + const syncMenuColorMix = useCallback(() => { if (!menuSync) { return; } - const updateMenuColorMix = () => { - const track = trackRef.current; - const trackRect = track?.getBoundingClientRect() ?? null; - const aiPanelRect = aiPanelRef.current?.getBoundingClientRect() ?? null; + const track = trackRef.current; + const trackRect = track?.getBoundingClientRect() ?? null; + const aiPanelRect = aiPanelRef.current?.getBoundingClientRect() ?? null; - menuSync.setMenuColorMix( - getHeroMenuColorMix({ - aiPanelRect, - colorMix, - isScrollDriven, - navHeight: NAV_HEIGHT, - panelMix, - trackRect, - viewportHeight: window.innerHeight, - }), - ); - }; + menuSync.setMenuDarkOpacity( + getHeroMenuColorMix({ + aiPanelRect, + colorMix, + isScrollDriven, + navHeight: NAV_HEIGHT, + panelMix, + trackRect, + viewportHeight: window.innerHeight, + }), + ); + }, [colorMix, isScrollDriven, menuSync, panelMix]); - updateMenuColorMix(); - window.addEventListener('scroll', updateMenuColorMix, { passive: true }); - window.addEventListener('resize', updateMenuColorMix); + useEffect(() => { + syncMenuColorMix(); + window.addEventListener('scroll', syncMenuColorMix, { passive: true }); + window.addEventListener('resize', syncMenuColorMix); return () => { - window.removeEventListener('scroll', updateMenuColorMix); - window.removeEventListener('resize', updateMenuColorMix); + window.removeEventListener('scroll', syncMenuColorMix); + window.removeEventListener('resize', syncMenuColorMix); }; - }, [colorMix, isScrollDriven, menuSync, panelMix]); + }, [syncMenuColorMix]); return ( @@ -393,7 +393,7 @@ export function HeroVisualScroll({ > diff --git a/packages/twenty-website/src/sections/Hero/components/ProductVisual/hero-scroll-colors.ts b/packages/twenty-website/src/sections/Hero/components/ProductVisual/hero-scroll-colors.ts index 2743b95ad1d..2a52716bf18 100644 --- a/packages/twenty-website/src/sections/Hero/components/ProductVisual/hero-scroll-colors.ts +++ b/packages/twenty-website/src/sections/Hero/components/ProductVisual/hero-scroll-colors.ts @@ -51,7 +51,7 @@ function smoothstep(value: number): number { return value * value * (3 - 2 * value); } -// Dark reads as a layer fading in over white — ramps up slowly, then completes. +// Dark reads as a layer fading in over white — ramps up slowly on scroll. function mapColorMixToDarkOpacity(colorMix: number): number { if (colorMix <= 0) { return 0; @@ -64,6 +64,17 @@ function mapColorMixToDarkOpacity(colorMix: number): number { return Math.pow(colorMix, 1.85); } +function getDarkOverlayOpacity(colorMix: number, panelMix: number): number { + const fromScroll = mapColorMixToDarkOpacity(colorMix); + const fromPanel = mapColorMixToDarkOpacity(panelMix); + + if (panelMix >= 0.5) { + return Math.max(fromScroll, fromPanel); + } + + return fromScroll; +} + function parseHexColor(hex: string): [number, number, number] { const normalized = hex.replace('#', ''); @@ -114,9 +125,7 @@ function interpolateRgba(from: string, to: string, mix: number): string { return `rgba(${red}, ${green}, ${blue}, ${alpha})`; } -export function getHeroScrollColors(colorMix: number) { - const darkOverlayOpacity = mapColorMixToDarkOpacity(colorMix); - +function buildHeroScrollColors(darkOverlayOpacity: number) { return { backgroundColor: LIGHT_BACKGROUND, darkOverlayOpacity, @@ -135,6 +144,14 @@ export function getHeroScrollColors(colorMix: number) { }; } +export function getHeroScrollColors(colorMix: number, panelMix = colorMix) { + return buildHeroScrollColors(getDarkOverlayOpacity(colorMix, panelMix)); +} + +export function getHeroScrollColorsFromOpacity(darkOverlayOpacity: number) { + return buildHeroScrollColors(darkOverlayOpacity); +} + export function getHeroMenuScheme(darkOverlayOpacity: number) { return darkOverlayOpacity >= 0.55 ? 'secondary' : 'primary'; } @@ -187,10 +204,10 @@ export function getHeroMenuColorMix({ return Math.min(1, visibilityRatio) * smoothstep(Math.min(1, exitFade)); } - let menuColorMix = colorMix; + let menuDarkOpacity = getDarkOverlayOpacity(colorMix, panelMix); if (panelMix < 0.5) { - menuColorMix = colorMix * (panelMix / 0.5); + menuDarkOpacity = getDarkOverlayOpacity(colorMix * (panelMix / 0.5), panelMix); } const isExitingHero = @@ -202,10 +219,10 @@ export function getHeroMenuColorMix({ Math.max(viewportHeight - navHeight, 1); const exitFade = smoothstep(Math.min(1, Math.max(0, exitProgress))); - menuColorMix = menuColorMix * (1 - exitFade); + menuDarkOpacity = menuDarkOpacity * (1 - exitFade); } - return menuColorMix; + return menuDarkOpacity; } export function getHeroScrollMotion( diff --git a/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-menu-sync.tsx b/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-menu-sync.tsx index 7ad0bb73d27..f36023725e5 100644 --- a/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-menu-sync.tsx +++ b/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-menu-sync.tsx @@ -15,10 +15,13 @@ import type { MenuSocialLinkType, } from '@/sections/Menu/types'; -import { getHeroMenuScheme, getHeroScrollColors } from './hero-scroll-colors'; +import { + getHeroMenuScheme, + getHeroScrollColorsFromOpacity, +} from './hero-scroll-colors'; type ProductHeroMenuContextValue = { - setMenuColorMix: (colorMix: number) => void; + setMenuDarkOpacity: (darkOverlayOpacity: number) => void; }; const ProductHeroMenuContext = @@ -39,9 +42,9 @@ export function ProductHeroMenuSync({ navItems, socialLinks, }: ProductHeroMenuSyncProps) { - const [menuColorMix, setMenuColorMix] = useState(0); + const [menuDarkOpacity, setMenuDarkOpacity] = useState(0); - const heroMenuColors = getHeroScrollColors(menuColorMix); + const heroMenuColors = getHeroScrollColorsFromOpacity(menuDarkOpacity); const menuScheme: MenuScheme = getHeroMenuScheme( heroMenuColors.darkOverlayOpacity, ); @@ -49,7 +52,7 @@ export function ProductHeroMenuSync({ const contextValue = useMemo( () => ({ - setMenuColorMix, + setMenuDarkOpacity, }), [], ); diff --git a/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-tabs-id.ts b/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-tabs-id.ts new file mode 100644 index 00000000000..aac6c2e3d56 --- /dev/null +++ b/packages/twenty-website/src/sections/Hero/components/ProductVisual/product-hero-tabs-id.ts @@ -0,0 +1 @@ +export const PRODUCT_HERO_AI_TABS_ID = 'product-hero-ai-tabs';