feat(ui): 弹层进出场过渡 + 首屏暖深色英雄(P2-6/P4-6)

- useMountTransition hook(延迟卸载播放退场,尊重 reduced-motion)+ vitest
- Drawer 淡入+滑入进出场;focus/scroll-lock 仍由 open 驱动、focus 键 shouldRender 防丢首焦
- Toast 入场 toast-in 动画(motion-safe)
- P4-6 首屏零作品用 callout 暖深色编辑部英雄(对标 cta-band-dark,不干扰工作库)
  新增 callout/on-callout/on-callout-soft 双主题成对 token
This commit is contained in:
Yaojia Wang
2026-07-11 14:04:14 +02:00
parent 355a2d11cd
commit f4bea7f26d
7 changed files with 162 additions and 30 deletions

View File

@@ -0,0 +1,29 @@
"use client";
import { useEffect, useState } from "react";
// 弹层进出场:打开即挂载并在下一帧置为可见(触发入场过渡);关闭先置不可见播放出场,
// 延迟 durationMs 后再卸载。调用方用 motion-safe: 门控过渡类——reduced-motion 下无动画,
// 仍功能正常(仅多挂载 durationMs。守住既有 focus-trap/scroll-lock它们仍由 open 驱动,
// 本 hook 只负责“延迟卸载”以便播放退场。
export function useMountTransition(
isOpen: boolean,
durationMs: number,
): { shouldRender: boolean; isVisible: boolean } {
const [shouldRender, setShouldRender] = useState(isOpen);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (isOpen) {
setShouldRender(true);
// 下一帧再置可见,确保入场从初始(隐藏)态开始过渡。
const raf = requestAnimationFrame(() => setIsVisible(true));
return () => cancelAnimationFrame(raf);
}
setIsVisible(false);
const timer = window.setTimeout(() => setShouldRender(false), durationMs);
return () => window.clearTimeout(timer);
}, [isOpen, durationMs]);
return { shouldRender, isVisible };
}