Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tablet-two-column-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/ratewise': patch
---

平板與桌面(≥1024px)佈局改善:首頁換算器改兩欄(左欄換算核心、右欄常用貨幣與全部幣種)、隱私政策頁目錄轉左側錨點欄、幣別匯率頁六段內容轉寬版排列(快速答案與 CTA 並排、金額頁常見金額列表全幅),大幅減少寬視口留白;手機與直向平板佈局零變化。
15 changes: 11 additions & 4 deletions apps/ratewise/src/components/CurrencyLandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export function CurrencyLandingPage({
<SEOHelmet {...seoProps} />

<ContentPageLayout
width="wide"
Comment thread
s123104 marked this conversation as resolved.
breadcrumbItems={[
{ label: t('nav.home'), href: '/' },
{
Expand All @@ -227,7 +228,8 @@ export function CurrencyLandingPage({
]}
testId="currency-landing-page"
>
<div className="space-y-6">
{/* #594 二階:≥1024px 六段 IA 轉寬版兩欄 grid(全幅段落標 lg:col-span-2);<1024px 佈局零變化。 */}
<div className="space-y-6 lg:grid lg:grid-cols-2 lg:items-start lg:gap-6 lg:space-y-0">
{/* 1. Answer Hero:頁面身分+(金額頁換算結果)+快速答案+CTA。 */}
<CurrencyAnswerHero
flag={currencyFlag}
Expand Down Expand Up @@ -272,8 +274,13 @@ export function CurrencyLandingPage({
reverseLadder={reverseLadder}
/>
)}
{/* 金額頁另有階梯表佔一半欄(與報價對比卡成對),互鏈改全幅避免奇數子項右側懸空。 */}
{commonAmounts.length > 0 && (
<CommonAmountsSection commonAmounts={commonAmounts} pathname={pathname} />
<CommonAmountsSection
commonAmounts={commonAmounts}
pathname={pathname}
className={ladderRows.length > 0 ? 'lg:col-span-2' : undefined}
/>
)}

{/* 4. 在地情境卡片組:匯率重點+旅遊提示+換匯管道比較。 */}
Expand All @@ -296,7 +303,7 @@ export function CurrencyLandingPage({

{/* 5. FAQ 手風琴(E4 ContentFaqAccordion 範式)。 */}
{faqEntries.length > 0 && (
<section>
<section className="lg:col-span-2">
<CurrencySectionHeading icon={HelpCircle}>{faqTitle}</CurrencySectionHeading>
<ContentFaqAccordion items={faqEntries} />
</section>
Expand All @@ -306,7 +313,7 @@ export function CurrencyLandingPage({
{relatedGuides.length > 0 && <RelatedGuidesSection relatedGuides={relatedGuides} />}

{/* Data Source Notice */}
<footer className="text-center text-xs text-text-muted opacity-60">
<footer className="text-center text-xs text-text-muted opacity-60 lg:col-span-2">
<p>資料來源:臺灣銀行牌告匯率 · 每 5 分鐘自動更新</p>
<p className="mt-1">{getCopyrightNotice()}</p>
</footer>
Expand Down
10 changes: 8 additions & 2 deletions apps/ratewise/src/components/currency/CommonAmountsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ import { CurrencySectionHeading } from './CurrencySectionHeading';
export interface CommonAmountsSectionProps {
commonAmounts: CommonAmountEntry[];
pathname: string;
/** #594 二階:寬版 grid 欄位配置由頁面層決定(金額頁全幅防半欄懸空)。 */
className?: string;
}

export function CommonAmountsSection({ commonAmounts, pathname }: CommonAmountsSectionProps) {
export function CommonAmountsSection({
commonAmounts,
pathname,
className,
}: CommonAmountsSectionProps) {
const basePath = pathname.replace(/\/$/, '');

return (
<section>
<section className={className}>
<CurrencySectionHeading icon={Calculator}>常見金額換算</CurrencySectionHeading>
<div className="rounded-card border border-border/60 bg-surface p-4 shadow-card sm:p-5">
<p className="text-xs leading-relaxed text-text-muted sm:text-sm">
Expand Down
16 changes: 10 additions & 6 deletions apps/ratewise/src/components/currency/CurrencyAnswerHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ export function CurrencyAnswerHero({
children,
}: CurrencyAnswerHeroProps) {
return (
<section className="rounded-card border border-border/60 bg-surface p-5 shadow-card sm:p-6">
// #594 二階:≥1024px 佔滿兩欄寬版;<1024px 佈局零變化。
<section className="rounded-card border border-border/60 bg-surface p-5 shadow-card sm:p-6 lg:col-span-2">
<header className="flex items-start gap-3 sm:gap-4">
<span className="text-4xl leading-none sm:text-5xl">{flag}</span>
<div className="min-w-0 flex-1">
<h1 className="text-2xl font-bold leading-tight tracking-tight text-text sm:text-[28px]">
{heading}
</h1>
<p className="mt-2 text-sm leading-relaxed text-text-muted sm:text-base">{description}</p>
<p className="mt-2 text-sm leading-relaxed text-text-muted sm:text-base lg:max-w-[65ch]">
{description}
</p>
</div>
</header>

Expand All @@ -52,12 +55,13 @@ export function CurrencyAnswerHero({

{children}

{/* 單一帶參數 hero 卡(#631):快速答案+深連結 CTA 收斂同卡,砍除與快速答案語意重複的行銷段。 */}
<div className="mt-4 rounded-panel border border-primary/20 bg-primary/5 p-4">
{/* 單一帶參數 hero 卡(#631):快速答案+深連結 CTA 收斂同卡,砍除與快速答案語意重複的行銷段。
#594 二階:≥1024px 快速答案與 CTA 並排(答案左、CTA 右置中);<1024px 佈局零變化。 */}
<div className="mt-4 rounded-panel border border-primary/20 bg-primary/5 p-4 lg:grid lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center lg:gap-x-6">
{quickAnswers.length > 0 && (
<>
<h2 className="text-sm font-semibold text-primary-on-surface">快速答案</h2>
<div className="mt-3 space-y-3">
<div className="mt-3 space-y-3 lg:col-start-1 lg:max-w-[65ch]">
{quickAnswers.map((item) => (
<div
key={item.question}
Expand All @@ -74,7 +78,7 @@ export function CurrencyAnswerHero({
)}
<Link
to={ctaTo}
className="mt-4 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-control bg-primary-strong px-5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary-hover sm:w-auto"
className="mt-4 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-control bg-primary-strong px-5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary-hover sm:w-auto lg:col-start-2 lg:row-start-1 lg:row-span-2 lg:mt-0 lg:self-center"
>
{ctaLabel}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
Expand Down
3 changes: 2 additions & 1 deletion apps/ratewise/src/components/currency/HowToStepsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface HowToStepsSectionProps {

export function HowToStepsSection({ howToSteps }: HowToStepsSectionProps) {
return (
<section>
// #594 二階:≥1024px 佔滿兩欄寬版;<1024px 佈局零變化。
<section className="lg:col-span-2">
<CurrencySectionHeading icon={ListOrdered}>使用步驟</CurrencySectionHeading>
<div className="grid gap-3 md:grid-cols-2">
{howToSteps.map((step) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export function LocalInsightsSection({
travelTip,
}: LocalInsightsSectionProps) {
return (
<section>
// #594 二階:≥1024px 佔滿兩欄寬版;<1024px 佈局零變化。
<section className="lg:col-span-2">
<CurrencySectionHeading icon={BookOpen}>{currencyName}匯率重點</CurrencySectionHeading>
<div className="grid gap-3 md:grid-cols-3">
{highlights.map((highlight, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function ProviderComparisonSection({
alternativeProviders,
}: ProviderComparisonSectionProps) {
return (
<section data-testid="provider-comparison-card">
// #594 二階:≥1024px 佔滿兩欄寬版;<1024px 佈局零變化。
<section data-testid="provider-comparison-card" className="lg:col-span-2">
<CurrencySectionHeading icon={Landmark}>換匯管道比較</CurrencySectionHeading>
<div className="rounded-card border border-border/60 bg-surface p-4 shadow-card sm:p-5">
<h3 className="text-sm font-bold text-text sm:text-base">台銀 vs 現場換匯所比較</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export interface RelatedGuidesSectionProps {

export function RelatedGuidesSection({ relatedGuides }: RelatedGuidesSectionProps) {
return (
<section>
// #594 二階:≥1024px 佔滿兩欄寬版;<1024px 佈局零變化。
<section className="lg:col-span-2">
<CurrencySectionHeading icon={BookOpen}>相關攻略</CurrencySectionHeading>
<div className="rounded-card border border-border/60 bg-surface shadow-card">
<div className="divide-y divide-border/40">
Expand Down
6 changes: 4 additions & 2 deletions apps/ratewise/src/config/design-tokens/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,15 @@ export const rateWiseLayoutTokens = {
/**
* 內容容器 - 流體內距搭配最大寬度限制
* [align:2026-01-29] 與 3ea33 版一致的 px/py 與 max-w
* [#594 二階] ≥1024px 轉兩欄 grid:左欄換算核心(28rem)、右欄輔助資訊;<1024px 佈局零變化。
*/
content: {
className: 'flex-1 flex flex-col px-3 sm:px-5 py-4 max-w-md mx-auto w-full',
className:
'flex-1 flex flex-col px-3 sm:px-5 py-4 max-w-md mx-auto w-full lg:max-w-5xl lg:grid lg:grid-cols-[minmax(0,28rem)_minmax(0,1fr)] lg:items-start lg:gap-x-6',
},

section: {
className: 'flex-1 flex flex-col mb-4',
className: 'flex-1 flex flex-col mb-4 lg:col-start-1',
},

/** 單幣別卡片 - 移除 flex-1 避免過度拉伸 */
Expand Down
10 changes: 5 additions & 5 deletions apps/ratewise/src/features/ratewise/RateWise.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo
{/* H1 由 routes.tsx SEO wrapper 提供,此處不重複 */}
{/* 載入狀態提示 */}
{ratesLoading && (
<div className="text-center text-sm text-neutral-text-secondary py-2">
<div className="text-center text-sm text-neutral-text-secondary py-2 lg:col-span-2">
載入即時匯率中...
</div>
)}
Expand All @@ -215,7 +215,7 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo
<div
role="status"
data-testid="ratewise-stale-rates-warning"
className="mb-3 flex items-center gap-2 rounded-xl border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning-text"
className="mb-3 flex items-center gap-2 rounded-xl border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning-text lg:col-span-2"
>
<AlertCircle size={14} aria-hidden="true" />
<span>{t('errors.rateStaleWarning')}</span>
Expand Down Expand Up @@ -251,8 +251,8 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo
</div>
</section>

{/* 收藏與貨幣列表區塊(桌面版顯示於側欄) */}
<section className="mb-4 hidden md:block flex-shrink-0">
{/* 收藏與貨幣列表區塊(md 起顯示;#594 二階:≥1024px 移入右欄輔助資訊) */}
<section className="mb-4 hidden md:block flex-shrink-0 lg:col-start-2 lg:row-span-2">
<div className="space-y-4">
<FavoritesList favorites={favorites} exchangeRates={exchangeRates} />
<CurrencyList
Expand All @@ -266,7 +266,7 @@ const RateWise = ({ rememberConverterView = true }: { rememberConverterView?: bo
{/* 資料來源與更新時間區塊 - 固定最小高度避免載入完成後 footer 位移 */}
<section
data-testid="ratewise-data-source"
className={`${rateWiseLayoutTokens.info.base} ${rateWiseLayoutTokens.info.visibility} min-h-6`}
className={`${rateWiseLayoutTokens.info.base} ${rateWiseLayoutTokens.info.visibility} min-h-6 lg:col-start-1`}
>
{!ratesLoading && lastUpdate ? (
<AnimatePresence mode="wait">
Expand Down
30 changes: 18 additions & 12 deletions apps/ratewise/src/pages/Privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,25 @@ export default function Privacy() {
>
<ContentPageHeader title="隱私政策" meta={<>最後更新:{LAST_UPDATED}</>} />

<nav aria-label="頁內目錄" className="mb-6 flex flex-wrap gap-2">
{TOC.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className="inline-flex min-h-11 items-center rounded-full border border-border/60 bg-surface px-4 text-sm font-medium text-text-muted transition-colors hover:bg-primary/5 hover:text-primary-on-surface"
>
{item.title}
</a>
))}
</nav>
{/* #594 二階:≥1024px 目錄轉左側錨點欄+正文收斂閱讀寬度;<1024px 佈局零變化。 */}
<div className="lg:grid lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start lg:gap-6">
Comment thread
s123104 marked this conversation as resolved.
<nav
aria-label="頁內目錄"
className="mb-6 flex flex-wrap gap-2 lg:sticky lg:top-16 lg:mb-0 lg:flex-col lg:self-start"
>
{TOC.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className="inline-flex min-h-11 items-center rounded-full border border-border/60 bg-surface px-4 text-sm font-medium text-text-muted transition-colors hover:bg-primary/5 hover:text-primary-on-surface"
>
{item.title}
</a>
))}
</nav>

<ContentSections sections={SECTIONS} />
<ContentSections sections={SECTIONS} />
</div>

<p className="mt-8 text-center text-sm text-text-muted">
© {getCopyrightYears()} {APP_INFO.name}
Expand Down
7 changes: 6 additions & 1 deletion docs/dev/002_development_reward_penalty_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> 版本:outline-v2-ultra
> 原則:每筆只保留日期、ID、原因、解法。
> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+161
> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+162

## 新增模板(4 行)

Expand All @@ -13,6 +13,11 @@

## 條目(新→舊)

- 日期:2026-07-06
- ID:reward-rw-594-tablet-two-column
- 原因:≥1024px 視口主內容仍為手機版單欄置中(首頁 max-w-md、幣別頁 max-w-3xl),12.9" 橫向留白 >60%(issue #594 二階、E6 評分卡實測)
- 解法:純 lg: class 收斂——首頁 grid 兩欄(換算核心 28rem+右欄收藏/幣種、v1/v2 共用且 keypad 常駐不動)、隱私頁目錄轉 sticky 錨點欄、幣別頁 wide 容器+六段 IA 寬版(快速答案+CTA 並排、金額頁常見金額互鏈全幅防半欄懸空);<1024px 佈局零變化(像素 diff AE=0)、SSG 非目標頁 13 頁零 diff

- 日期:2026-07-06
- ID:reward-rw-609-builtin-theme-aa-batch
- 原因:nitro primary-hover 白字僅 ~1.8:1、小字白字按鈕 3.23:1(守門鎖 3:1 放行);kawaii hot pink 對白底 2.65:1 經 on-surface==primary 合約傳導到全站文字;`/70`、`opacity-[0.35]` alpha 疊加把達標實色稀釋到 <4.5:1 且守門只驗實色
Expand Down
83 changes: 83 additions & 0 deletions scripts/qa/issue-594-screenshot-matrix.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* issue #594 二階平板適配截圖矩陣:
* {首頁 v1, 首頁 v2, guide, privacy, jpy-twd} × {768×1024, 1024×768, 1440×900, 390×844, 375×667}。
* 同時蒐集 console error。輸出至 repo 根 screenshots/,檔名前綴由 PHASE 環境變數控制(before/after)。
* 用法:PHASE=before node scripts/qa/issue-594-screenshot-matrix.mjs(需先啟動 preview :4173)
*/

import { chromium } from '@playwright/test';
import { mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const OUT_DIR = join(ROOT, 'screenshots');
const BASE = process.env.BASE_URL ?? 'http://localhost:4173';
const PHASE = process.env.PHASE ?? 'before';

const PAGES = [
['home-v1', '/ratewise/'],
['home-v2', '/ratewise/?converter=v2'],
['guide', '/ratewise/guide/'],
['privacy', '/ratewise/privacy/'],
['jpy-twd', '/ratewise/jpy-twd/'],
];
const VIEWPORTS = [
[768, 1024],
[1024, 768],
[1440, 900],
[390, 844],
[375, 667],
];

mkdirSync(OUT_DIR, { recursive: true });

const browser = await chromium.launch();
const consoleErrors = [];

for (const [width, height] of VIEWPORTS) {
const isMobile = width < 768;
const context = await browser.newContext({
viewport: { width, height },
isMobile,
hasTouch: isMobile,
deviceScaleFactor: 2,
});
await context.addInitScript(() => {
// 跳過啟動畫面等待,穩定截圖內容。
sessionStorage.setItem('ratewise:splash-shown', '1');
});

for (const [name, path] of PAGES) {
const page = await context.newPage();
const label = `${name}-${width}x${height}`;
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(`[${label}] ${message.text()}`);
}
});
page.on('pageerror', (error) => {
consoleErrors.push(`[${label}] pageerror: ${error.message}`);
});

await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
await page.waitForTimeout(1200);
await page.screenshot({ path: join(OUT_DIR, `594-${PHASE}-${label}.png`) });
await page.close();
console.log(`captured 594-${PHASE}-${label}.png`);
}
await context.close();
}

await browser.close();

// 已知基線問題:SSG hydration React #418(基底同樣出現,與本次無關)。
const KNOWN_BASELINE = /Minified React error #418/;
const newErrors = consoleErrors.filter((entry) => !KNOWN_BASELINE.test(entry));
console.log(`\nknown baseline hydration #418: ${consoleErrors.length - newErrors.length}`);
if (newErrors.length > 0) {
console.error(`console errors (excluding known baseline): ${newErrors.length}`);
for (const entry of newErrors) console.error(entry);
process.exit(1);
}
console.log('console errors (excluding known baseline): 0');
Loading