From 1b4bddba6f49a6b5ac3b8fc2f6aaa3ce534c4efb Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:43:45 +0500 Subject: [PATCH] fix(website): polish product hero AI transition (panel reveal timing + title wrapping) (#22223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two desktop/tablet polish refinements to the product page hero's hero→AI transition (`product-hero`). ### 1. Delay the Ask-AI panel reveal to after the wipe (`b638d600`) The Ask-AI side panel used to reveal *during* the dark "wipe up" (keyed to the morph `0.45 → 0.70`), so it competed with the rising black edge and piled onto the heaviest motion phase — rough on weaker laptops. It's now keyed to raw scroll progress over `[0.60, 0.70]` — the previously-idle hold after the wipe settles at `0.55` — so it slides in on its own beat once the eye has followed the black up. `morphProgress` is pinned at `1` across the whole post-wipe stretch, so leaving the morph clock was the only way to time the panel *after* the wipe. The conversation playback moves with it, so the chat doesn't stream while the panel is still `width: 0`. ### 2. Keep the AI heading to three lines below desktop (`8ae69a4b`) The AI heading is the page's longest line. Below `md`, only the mobile copy renders, where the measure was frozen at `360px` while the heading font scales fluidly toward `md` — so on tablet widths the title crammed onto four lines, recovering only at `921px` when the `672px` measure kicks in. Adds an `sm`-breakpoint measure step (`560px`) scoped to the AI heading; the intro heading and the desktop measuring path keep their existing `360/672` steps. ## Test plan - `product-hero-scroll-model` jest suite updated and green (11/11), including a new post-morph panel-timing test. - Lint (`check-conventions` + `oxlint` + `oxfmt`) and typecheck green. - Visually confirmed: the panel reveals after the wipe settles, and the AI title holds three lines across tablet widths. --- .../product-hero/HeroVisualScroll.tsx | 8 +++++++- .../product-hero-scroll-model.test.ts | 20 ++++++++++++++++--- .../product-hero/product-hero-scroll-model.ts | 15 ++++++++++++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/twenty-website/src/sections/product-hero/HeroVisualScroll.tsx b/packages/twenty-website/src/sections/product-hero/HeroVisualScroll.tsx index 9b3820aa92..e7c49d16eb 100644 --- a/packages/twenty-website/src/sections/product-hero/HeroVisualScroll.tsx +++ b/packages/twenty-website/src/sections/product-hero/HeroVisualScroll.tsx @@ -214,6 +214,12 @@ const HeadingSlot = styled.div` position: relative; width: 100%; + ${mediaUp('sm')} { + &[data-measure='ai'] { + max-width: 560px; + } + } + ${mediaUp('md')} { max-width: 672px; } @@ -648,7 +654,7 @@ export function HeroVisualScroll({ - + {aiHeading} diff --git a/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.test.ts b/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.test.ts index f152d76652..03501b204d 100644 --- a/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.test.ts +++ b/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.test.ts @@ -30,11 +30,13 @@ describe('computeHeroScrollModel', () => { it('should complete the morph at 55% of the scrollable run', () => { const model = atScroll(0.55); expect(model.morphProgress).toBe(1); - expect(model.aiPanelProgress).toBe(1); - expect(model.aiPlaybackEnabled).toBe(true); expect(model.selectorRevealReady).toBe(true); expect(model.stackSpreadProgress).toBe(1); expect(model.stackSpreadEasedProgress).toBe(1); + // The Ask-AI panel is deliberately still closed here; it reveals only + // in the post-morph hold (see the panel-timing test below). + expect(model.aiPanelProgress).toBe(0); + expect(model.aiPlaybackEnabled).toBe(false); }); it('should ease the morph through smoothstep', () => { @@ -51,7 +53,19 @@ describe('computeHeroScrollModel', () => { expect(half.stackAppearProgress).toBeCloseTo((0.5 - 0.4) / 0.16, 10); expect(half.stackAlignProgress).toBe(0); expect(half.stackSpreadProgress).toBe(0); - expect(half.aiPanelProgress).toBeCloseTo((0.5 - 0.45) / 0.25, 10); + }); + + it('should hold the Ask-AI panel back until the post-morph hold', () => { + // The wipe finishes at scroll 0.55; the panel reveals over [0.60, 0.70] + // of raw scroll progress, then holds open, and playback begins only + // once it has fully opened. + expect(atScroll(0.55).aiPanelProgress).toBe(0); + expect(atScroll(0.55).aiPlaybackEnabled).toBe(false); + expect(atScroll(0.65).aiPanelProgress).toBeCloseTo(0.5, 10); + expect(atScroll(0.65).aiPlaybackEnabled).toBe(false); + expect(atScroll(0.7).aiPanelProgress).toBe(1); + expect(atScroll(0.7).aiPlaybackEnabled).toBe(true); + expect(atScroll(1).aiPanelProgress).toBe(1); }); it('should gate the AI layer at the wipe midpoint', () => { diff --git a/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.ts b/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.ts index f45c52ad1e..0163eb3122 100644 --- a/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.ts +++ b/packages/twenty-website/src/sections/product-hero/product-hero-scroll-model.ts @@ -40,6 +40,14 @@ const NAV_HEIGHT_PX = 64; const MORPH_START = 0; const MORPH_END = 0.55; +// The Ask-AI panel holds back until the wipe has fully risen, then slides +// in during the post-morph hold as its own beat — so the eye, having +// followed the black edge up, lands on the panel instead of competing +// with it. Keyed to raw scroll progress: morph is pinned at 1 through +// this whole stretch and so can't time anything past MORPH_END. +const AI_PANEL_REVEAL_START = 0.6; +const AI_PANEL_REVEAL_END = 0.7; + function smoothstep(value: number): number { return value * value * (3 - 2 * value); } @@ -102,8 +110,11 @@ export function computeHeroScrollModel({ const selectorRevealProgress = clampProgress((morphProgress - 0.94) / 0.06); return { - aiPanelProgress: clampProgress((morphProgress - 0.45) / 0.25), - aiPlaybackEnabled: morphProgress >= 0.7, + aiPanelProgress: clampProgress( + (progress - AI_PANEL_REVEAL_START) / + (AI_PANEL_REVEAL_END - AI_PANEL_REVEAL_START), + ), + aiPlaybackEnabled: progress >= AI_PANEL_REVEAL_END, aiPointerEventsEnabled: morphProgress > 0.5, heroAtStart: morphProgress <= 0, introCursorsActive: morphProgress < 0.5,