diff --git a/.changeset/ratewise-homepage-cls-454.md b/.changeset/ratewise-homepage-cls-454.md new file mode 100644 index 000000000..23761306e --- /dev/null +++ b/.changeset/ratewise-homepage-cls-454.md @@ -0,0 +1,5 @@ +--- +'@app/ratewise': patch +--- + +首頁換算器預留固定高度,減少匯率卡片與計價基準 pill 載入時的版面跳動(CLS) diff --git a/apps/ratewise/src/config/design-tokens.ts b/apps/ratewise/src/config/design-tokens.ts index a4935f7e5..dd93f1086 100644 --- a/apps/ratewise/src/config/design-tokens.ts +++ b/apps/ratewise/src/config/design-tokens.ts @@ -711,6 +711,17 @@ export const singleConverterLayoutTokens = { /** 次要匯率文字 */ rateSubText: 'text-sm short:text-xs tiny:text-xs micro:text-[10px] nano:text-[10px]', + /** 匯率文字區塊最小高度(含計價基準 pill 槽位,避免 async 載入位移) */ + rateTextBlock: + 'min-h-[5rem] compact:min-h-[4.5rem] short:min-h-[4rem] tiny:min-h-[3.75rem] micro:min-h-[3.5rem] nano:min-h-[3.25rem]', + + /** 計價基準 pill 槽位(無 pill 時仍保留高度) */ + rateBasisSlot: 'mt-1.5 flex min-h-6 items-center justify-center', + + /** 匯率卡片本體最小高度(資訊區 + 圖表) */ + cardMinHeight: + 'min-h-[10.5rem] compact:min-h-[9.5rem] short:min-h-[8.75rem] tiny:min-h-[8rem] micro:min-h-[7.25rem] nano:min-h-[6.75rem]', + /** 趨勢圖高度 - 線性遞減 */ chartHeight: 'h-20 compact:h-16 short:h-14 tiny:h-12 micro:h-10 nano:h-8', diff --git a/apps/ratewise/src/features/ratewise/RateWise.tsx b/apps/ratewise/src/features/ratewise/RateWise.tsx index 83bb89db6..8c4a291ec 100644 --- a/apps/ratewise/src/features/ratewise/RateWise.tsx +++ b/apps/ratewise/src/features/ratewise/RateWise.tsx @@ -263,12 +263,12 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo - {/* 資料來源與更新時間區塊 - 現代化精簡設計,固定在底部 */} - {!ratesLoading && lastUpdate && ( -
+ {/* 資料來源與更新時間區塊 - 固定最小高度避免載入完成後 footer 位移 */} +
+ {!ratesLoading && lastUpdate ? ( {effectiveRateSource === 'exchange-shop' && moneyBoxRate?.currency === exchangeShopCurrency ? ( @@ -292,8 +292,12 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo )} -
- )} + ) : ( + + )} +
diff --git a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx index 272240b98..a3ef4e831 100644 --- a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx +++ b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx @@ -117,7 +117,6 @@ export const SingleConverter = ({ const [trendData, setTrendData] = useState([]); const [_loadingTrend, setLoadingTrend] = useState(false); const [isSwapping, setIsSwapping] = useState(false); - const [showTrend, setShowTrend] = useState(false); const [trendDateKey, setTrendDateKey] = useState(() => getLocalDateKey()); const swapButtonRef = useRef(null); @@ -157,12 +156,6 @@ export const SingleConverter = ({ ); const reverseRate = getReciprocalExchangeRate(exchangeRate); - // 趨勢圖進場動畫 - useEffect(() => { - const timer = setTimeout(() => setShowTrend(true), 300); - return () => clearTimeout(timer); - }, []); - // 處理交換按鈕點擊 const handleSwap = () => { setIsSwapping(true); @@ -486,7 +479,7 @@ export const SingleConverter = ({
{/* 匯率卡片 - 一體化設計,無切分感 */}
{/* * 微光效果 - 極淺的漸層覆蓋 @@ -509,8 +502,8 @@ export const SingleConverter = ({ onRateSourceChange={onRateSourceChange ?? (() => undefined)} /> - {/* 匯率顯示 - 使用 SSOT text 色 */} -
+ {/* 匯率顯示 - 使用 SSOT text 色;固定高度避免計價基準 pill / live 匯率載入 CLS */} +
@@ -521,15 +514,21 @@ export const SingleConverter = ({ > 1 {toCurrency} = {formatExchangeRate(reverseRate)} {fromCurrency}
+
{/* 滿版趨勢圖 - 無獨立背景,繼承父元素漸層實現一體化 */}
{ expect(trendChart).toHaveClass(className); }); }); + + it('匯率文字區塊應預留計價基準 pill 槽位高度', () => { + render(); + + const rateTextBlock = screen.getByText(/1 TWD =/).parentElement; + expect(rateTextBlock).toHaveClass(singleConverterLayoutTokens.rateCard.rateTextBlock); + }); }); describe('匯率類型切換', () => { @@ -433,21 +440,15 @@ describe('SingleConverter - 核心功能測試', () => { }); }); - describe('趨勢圖進場動畫', () => { - it('should show trend chart after 300ms delay', () => { + describe('趨勢圖 CLS 穩定', () => { + it('趨勢圖容器應自初始渲染起保持可見且固定高度', () => { render(); - // 初始狀態不應該顯示趨勢圖(opacity-0) - const trendContainer = document.querySelector('.opacity-0.translate-y-4'); - expect(trendContainer).toBeInTheDocument(); - - // 300ms 後應該顯示(opacity-100) - act(() => { - vi.advanceTimersByTime(400); + const trendChart = screen.getByTestId('trend-chart'); + expect(trendChart).not.toHaveClass('opacity-0'); + singleConverterLayoutTokens.rateCard.chartHeight.split(' ').forEach((className) => { + expect(trendChart).toHaveClass(className); }); - - const visibleTrend = document.querySelector('.opacity-100.translate-y-0'); - expect(visibleTrend).toBeInTheDocument(); }); }); diff --git a/docs/dev/002_development_reward_penalty_log.md b/docs/dev/002_development_reward_penalty_log.md index 37bc1f6ef..68190bc9a 100644 --- a/docs/dev/002_development_reward_penalty_log.md +++ b/docs/dev/002_development_reward_penalty_log.md @@ -2,7 +2,7 @@ > 版本:outline-v2-ultra > 原則:每筆只保留日期、ID、原因、解法。 -> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+74 +> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+75 ## 新增模板(4 行) @@ -13,6 +13,11 @@ ## 條目(新→舊) +- 日期:2026-06-27 +- ID:reward-ratewise-homepage-cls-454-hotfix +- 原因:#441 計價基準 pill 與趨勢圖進場動畫使首頁 CLS 達 0.237,超過 LHCI 門檻 0.1 +- 解法:SingleConverter 匯率卡片固定 min-height、預留 pill 槽位、移除趨勢圖 translate 動畫;RateWise footer 保留 min-h-6 + - 日期:2026-06-27 - ID:reward-moneybox-schema-migration-cdn-fallback - 原因:MoneyBox fetch 僅比對匯率數字,schema v2 遷移未觸發 data commit/purge;jsDelivr @data 分支快取最長 12h