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.
This commit is contained in:
Abdullah
2026-05-20 04:22:02 +05:00
parent a4161833d0
commit e37517dc3d
5 changed files with 64 additions and 58 deletions
@@ -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 (
<svg
width={size}
@@ -16,19 +10,10 @@ export function EditIcon({ size, color, strokeWidth = 2 }: EditIconProps) {
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<g
clipPath={`url(#${clipPathId})`}
stroke={color}
strokeWidth={strokeWidth}
>
<g stroke={color} strokeWidth={strokeWidth}>
<path d="M7.604 3.621c-.744.034-1.476.112-2.19.194-.96.11-1.729.88-1.835 1.84-.104.94-.2 1.911-.2 2.904s.096 1.963.2 2.904c.106.96.875 1.729 1.835 1.839.946.109 1.922.211 2.919.211s1.973-.102 2.92-.211a2.094 2.094 0 0 0 1.834-1.839 35 35 0 0 0 .178-2.032" />
<path d="M11.916 2.61 8.847 6.09a1.74 1.74 0 0 0-.394.791L8.087 8.6c-.074.349.29.68.63.572l1.724-.542c.287-.09.541-.247.743-.46l3.171-3.336c.578-.608.477-1.625-.222-2.243-.683-.604-1.676-.596-2.217.018Z" />
</g>
<defs>
<clipPath id={clipPathId}>
<path fill="#fff" d="M0 0h16v16H0z" />
</clipPath>
</defs>
</svg>
);
}
@@ -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 (
<ScrollTrack ref={trackRef}>
@@ -393,7 +393,7 @@ export function HeroVisualScroll({
>
<TabButtons
activeIndex={activeTab}
idPrefix={idPrefix}
idPrefix={PRODUCT_HERO_AI_TABS_ID}
onSelect={setActiveTab}
tabs={tabs}
/>
@@ -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(
@@ -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,
}),
[],
);
@@ -0,0 +1 @@
export const PRODUCT_HERO_AI_TABS_ID = 'product-hero-ai-tabs';