From ececf8c170f786d94b6f8e1e1ca18692faaacab5 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 28 Aug 2026 19:29:31 +0800 Subject: [PATCH] fix(web): address hero fish hover review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move hover enter/leave from the svg element to the stationary fishHitbox span so the CSS sway and SMIL morph share the same trigger surface and the animation cannot flicker when sway displaces the svg from the pointer. Gate the morph with (hover: hover) to match the CSS and prevent tap-sticky loops on touch devices. Remove residual headline changes unrelated to this PR: white-space: pre on .headlineText, data-testid="hero-headline", and corresponding getByTestId test assertions — these belong to #2397. Fix Agent Note coverage claim (was "asserting render states", now accurately "slot contract") and compress blowhole paragraph to current-state fact. --- ...08-12-hero-fish-hover-swim-morph.i18n.yaml | 4 +-- .../2026-08-12-hero-fish-hover-swim-morph.md | 4 +-- ...026-08-12-hero-fish-hover-swim-morph.zh.md | 4 +-- .../src/client/skeleton/EmptyHero.tsx | 30 ++++++++++--------- .../src/client/skeleton/HeroShell.module.css | 1 - .../tests/skeleton.client.spec.tsx | 6 ++-- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.i18n.yaml b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.i18n.yaml index 47ca707476..554c1976d5 100644 --- a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.md -2026-08-12-hero-fish-hover-swim-morph.md: 2ae090f2e9640fc68091e918a644b4f793d1a8f4 -2026-08-12-hero-fish-hover-swim-morph.zh.md: eac0047a083d410b1f538954088a29ef721fe054 +2026-08-12-hero-fish-hover-swim-morph.md: c485f94e244040d7e72838065f977e698391c00d +2026-08-12-hero-fish-hover-swim-morph.zh.md: 46fa50732bf98aa5e3afb503e84f8e23001499b2 diff --git a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.md b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.md index 2ae090f2e9..c485f94e24 100644 --- a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.md +++ b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.md @@ -18,10 +18,10 @@ The morphing fish reaches the hero as the fallback of the `conversation.hero.bra **Vector-tool path editing for the morphs.** No interactive tool in the loop; programmatic weighted deformation was chosen because it guarantees the identical command structure SMIL `d` interpolation requires and makes amplitudes reviewable numbers. -**Blowhole spout on hover.** Shipped first as three rising beads (richer whale-blow fountain, bubble-cluster, and ballistic-arc shapes were built and rejected in review), then removed entirely at the user's request: hover keeps only the shape morph and sway. +**Blowhole spout on hover.** Removed at the user's request; hover keeps only shape morph and sway. **Occupying the hero slot with the official mark.** The previous arrangement; rejected because the static occupant shadowed the animated fallback, and animating the occupant instead would need the forbidden cross-package value import. ## Consequences -The hover swim is decorative (`aria-hidden`) and reduced-motion-safe (static logo on hover). The sway CSS targets the stationary `.fishHitbox` wrapper, so a slot occupant would sway too; the body morph lives only in the fallback `HeroFish`. Coverage is the component suite (`skeleton.client.spec.tsx`) asserting render states; the keyless snapshot harness records transcripts, not browser animation, so visual verification stays manual. Regenerating the morph targets requires re-running the (uncommitted) deformation script against `FISH_LOGO_PATH`; if the logo geometry ever changes, the baked constants must be regenerated with it. +The hover swim is decorative (`aria-hidden`) and reduced-motion-safe (static logo on hover). The sway CSS targets the stationary `.fishHitbox` wrapper, so a slot occupant would sway too; the body morph lives only in the fallback `HeroFish`. Coverage is the `skeleton.client.spec.tsx` suite asserting slot contract (name, owner props, fallback existence); the keyless snapshot harness records transcripts, not browser animation, so visual verification of the morph stays manual. Regenerating the morph targets requires re-running the (uncommitted) deformation script against `FISH_LOGO_PATH`; if the logo geometry ever changes, the baked constants must be regenerated with it. diff --git a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.zh.md b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.zh.md index eac0047a08..46fa50732b 100644 --- a/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.zh.md +++ b/.agents/notes/implemented/feature/2026-08-12-hero-fish-hover-swim-morph.zh.md @@ -18,10 +18,10 @@ hover New Session 主页的鲸鱼(`dsh-client-ui-conversation` 的 `EmptyHero. **用矢量工具编辑路径做变形。** 流程中没有可交互的工具;选择程序化加权变形,因为它保证 SMIL `d` 插值所要求的完全一致的命令结构,且振幅是可评审的数字。 -**hover 气孔喷水。** 最初以三颗上升水珠的形式上线(更复杂的鲸鱼喷泉、泡泡簇、弹道水花造型都做出来后在评审中被否),随后按用户要求整体移除:hover 只保留形状变形与摇摆。 +**hover 气孔喷水。** 按用户要求移除;hover 只保留形状变形与摇摆。 **让官方标志占据主页 slot。** 即先前的安排;否决,因为静态 occupant 会遮住动画 fallback,而给 occupant 加动画又需要被禁止的跨包 value import。 ## 影响 -hover 游动是纯装饰(`aria-hidden`)且对 reduced-motion 安全(hover 保持静态 logo)。摇摆 CSS 作用于外层静止的 `.fishHitbox`,因此换成 slot occupant 也会摇摆;身体变形只存在于 fallback 的 `HeroFish` 中。覆盖由组件测试(`skeleton.client.spec.tsx`)断言渲染状态;keyless 快照体系记录的是对话转录而非浏览器动画,视觉验证仍需人工。重新生成变形目标需要对 `FISH_LOGO_PATH` 重跑(未入库的)变形脚本;若 logo 几何将来变化,烘焙常量必须随之重新生成。 +hover 游动是纯装饰(`aria-hidden`)且对 reduced-motion 安全(hover 保持静态 logo)。摇摆 CSS 作用于外层静止的 `.fishHitbox`,因此换成 slot occupant 也会摇摆;身体变形只存在于 fallback 的 `HeroFish` 中。覆盖由 `skeleton.client.spec.tsx` 断言 slot 合约(名称、owner props、fallback 存在性);keyless 快照体系记录对话转录而非浏览器动画,变形的视觉验证仍需人工。重新生成变形目标需要对 `FISH_LOGO_PATH` 重跑(未入库的)变形脚本;若 logo 几何将来变化,烘焙常量必须随之重新生成。 diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index 6cbf63a20b..90c8a32140 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -126,13 +126,12 @@ const HERO_SWIM_DOWN_PATH = * itself morphs — SMIL interpolates `d` through the tail-up and tail-down * targets on the same 1.6s period, so the tail wags and the fin flutters in * real curve deformation. Decorative — hidden from the accessibility tree; - * reduced motion keeps the static filled logo on hover. + * reduced motion keeps the static filled logo on hover (sampled at + * mouseenter; a mid-hover preference change takes effect on the next enter). + * @param props.hovering - driven by the hitbox parent's pointer state. * @returns the fish svg element. */ -function HeroFish() { - // Hover echo for the SMIL body morph: SMIL cannot ride CSS media queries, - // so the reduced-motion check gates the state instead of the style. - const [hovering, setHovering] = useState(false) +function HeroFish({ hovering }: { hovering: boolean }) { return (
{/* figma 34:10412: fish 34×25 leading the headline, gap 10. */} - + { + if (window.matchMedia('(hover: hover) and (prefers-reduced-motion: no-preference)').matches) { + setHovering(true) + } + }} + onMouseLeave={() => { setHovering(false) }} + > {renderSlot('conversation.hero.brand.mark', { size: 34, className: css.fish }, { - fallback: , + fallback: , })} - + {t('hero.headline')} {t('hero.preview')} diff --git a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css index f07152b3d2..889a140fe6 100644 --- a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css @@ -41,7 +41,6 @@ .headlineText { grid-row: 1; grid-column: 2; - white-space: pre; } .previewBadge { diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index d40e0d1ed1..ed8a222e4f 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -311,7 +311,7 @@ describe('Hero chrome', () => { it('renders the English preview badge through the hero locale seat', () => { const renderSlot = vi.fn(() => null) const view = render() - expect(view.getByTestId('hero-headline').textContent).toBe('Into the Unknown') + expect(view.getByText('Into the Unknown')).toBeTruthy() expect(view.getByText('Preview')).toBeTruthy() expect(renderSlot).toHaveBeenCalledOnce() expect(renderSlot.mock.calls[0]?.[0]).toBe('conversation.hero.brand.mark') @@ -439,7 +439,7 @@ describe('ConversationRoot resident composer', () => { const header = b.view.container.querySelector('header') expect(host).not.toBeNull() expect(header?.getAttribute('aria-hidden')).toBe('true') - expect(b.view.getByTestId('hero-headline').textContent).toBe('探索未至之境') + expect(b.view.getByText('探索未至之境')).toBeTruthy() expect(b.view.getByText('预览版')).toBeTruthy() expect(b.view.queryByTestId('view-chat')).toBeNull() // The same machine-backed textarea is live in the hero, and the @@ -505,7 +505,7 @@ describe('ConversationRoot resident composer', () => { // blank the column for the history round-trip. const root = b.view.container.querySelector('[data-phase]') expect(root?.getAttribute('data-phase')).toBe('hero') - expect(b.view.getByTestId('hero-headline').textContent).toBe('探索未至之境') + expect(b.view.getByText('探索未至之境')).toBeTruthy() expect(b.view.getByRole('textbox')).toBeTruthy() })