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
3 changes: 3 additions & 0 deletions e2e/stage-profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ test('上傳 GPX → 生成剖面圖與爬坡分級 → 下載 PNG', async ({ pa
// 出圖主題切換 → SVG 底色跟著換(環義粉 #FFF6FA)
await page.getByRole('button', { name: '環義粉' }).click();
await expect(page.getByRole('img', { name: '賽段剖面圖' }).locator('rect').first()).toHaveAttribute('fill', '#FFF6FA');
// 深色主題(午夜黑 #171A21)
await page.getByRole('button', { name: '午夜黑' }).click();
await expect(page.getByRole('img', { name: '賽段剖面圖' }).locator('rect').first()).toHaveAttribute('fill', '#171A21');
await page.getByRole('button', { name: 'Tiglet 暖橘' }).click();
await expect(page.getByRole('img', { name: '賽段剖面圖' }).locator('rect').first()).toHaveAttribute('fill', '#FAF9F5');

Expand Down
50 changes: 50 additions & 0 deletions src/lib/__tests__/profileThemes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import { THEMES, THEME_EN } from '../profileThemes';
import { contrastRatio, relativeLuminance } from '../contrast';

// 出圖主題的可讀性守門:新主題(尤其深色)不能比現有主題最低水準更差。
// 門檻取自現況實測的下限(環義粉 muted 4.49、環法黃 accent 2.23),
// 不是理想值 —— 收緊門檻前要先動既有主題。

describe('profile themes', () => {
it('每個主題都有完整色票與英文名', () => {
for (const th of THEMES) {
expect(th.id).toMatch(/^[a-z]+$/);
expect(th.label.length).toBeGreaterThan(0);
expect(THEME_EN[th.id], `THEME_EN 缺 ${th.id}`).toBeTruthy();
for (const c of [th.bg, th.ink, th.muted, th.grid, th.accent]) {
expect(c).toMatch(/^#[0-9A-Fa-f]{6}$/);
}
}
});

it('id 不重複', () => {
const ids = THEMES.map((t) => t.id);
expect(new Set(ids).size).toBe(ids.length);
});

it('標題與座標文字在底色上讀得清楚', () => {
for (const th of THEMES) {
expect(contrastRatio(th.ink, th.bg), `${th.id} ink`).toBeGreaterThanOrEqual(7);
expect(contrastRatio(th.muted, th.bg), `${th.id} muted`).toBeGreaterThanOrEqual(4.4);
expect(contrastRatio(th.accent, th.bg), `${th.id} accent`).toBeGreaterThanOrEqual(2.2);
}
});

it('格線壓得住但看得見(比底色明顯、比正文淡)', () => {
for (const th of THEMES) {
const grid = contrastRatio(th.grid, th.bg);
expect(grid, `${th.id} grid too faint`).toBeGreaterThanOrEqual(1.2);
expect(grid, `${th.id} grid too loud`).toBeLessThan(contrastRatio(th.muted, th.bg));
}
});

it('至少有一個深色主題(透明匯出疊深色背景時字才看得見)', () => {
const dark = THEMES.filter((t) => relativeLuminance(t.bg) < 0.2);
expect(dark.length).toBeGreaterThanOrEqual(1);
for (const th of dark) {
// 深色主題的墨色必須是淺色,去背 PNG 疊在深色簡報/限動上才可讀
expect(relativeLuminance(th.ink), `${th.id} ink 應為淺色`).toBeGreaterThan(0.7);
}
});
});
31 changes: 31 additions & 0 deletions src/lib/profileThemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 賽段剖面圖的出圖主題(純資料,供 StageProfile 與測試共用)。
// SVG 用固定色票(非 CSS 變數),匯出的 PNG 才不會受網站深淺色主題影響。
// 出圖主題只換底/墨/點綴色,坡度色階是語意(多陡),不隨主題變。

export interface ProfileTheme {
id: string;
label: string;
bg: string;
ink: string;
muted: string;
grid: string;
accent: string;
}

export const THEMES: ProfileTheme[] = [
{ id: 'tiglet', label: 'Tiglet 暖橘', bg: '#FAF9F5', ink: '#1A1A18', muted: '#6B6A63', grid: '#D6D1C4', accent: '#D97757' },
{ id: 'tour', label: '環法黃', bg: '#FFFFFF', ink: '#14141E', muted: '#5A5A66', grid: '#E2E2E8', accent: '#D4A800' },
{ id: 'giro', label: '環義粉', bg: '#FFF6FA', ink: '#2B1A22', muted: '#8A6A78', grid: '#F0D4E2', accent: '#E5518D' },
{ id: 'vuelta', label: '環西紅', bg: '#FFFAF6', ink: '#241514', muted: '#8A6A62', grid: '#F0DCD0', accent: '#DA291C' },
// 深色主題:墨色是淺色,所以「去背匯出疊在深色背景」也讀得清楚
{ id: 'night', label: '午夜黑', bg: '#171A21', ink: '#F0EEE6', muted: '#A9A79E', grid: '#3A3E48', accent: '#E8956D' },
];

// 出圖主題名稱的英文(按鈕 UI 用;id 對映,不動 THEMES 本體)
export const THEME_EN: Record<string, string> = {
tiglet: 'Tiglet warm',
tour: 'Tour yellow',
giro: 'Giro pink',
vuelta: 'Vuelta red',
night: 'Midnight black',
};
30 changes: 2 additions & 28 deletions src/tools/StageProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ShareLinkButton from '../components/ShareLinkButton';
import Tabs from '../components/Tabs';
import Toggle from '../components/Toggle';
import { simplifyProfile, encodeRouteShare, decodeRouteShare } from '../lib/routeShare';
import { THEMES, THEME_EN, type ProfileTheme } from '../lib/profileThemes';
import type { Locale } from '../lib/i18n';
import {
parseGpx,
Expand Down Expand Up @@ -312,34 +313,7 @@ const L = {

type Dict = (typeof L)[Locale];

// 出圖主題名稱的英文(按鈕 UI 用;id 對映,不動 THEMES 本體)
const THEME_EN: Record<string, string> = {
tiglet: 'Tiglet warm',
tour: 'Tour yellow',
giro: 'Giro pink',
vuelta: 'Vuelta red',
};

// 檔案完全在本機解析;SVG 用固定色票(非 CSS 變數),
// 匯出的 PNG 才不會受深淺色主題影響。出圖主題只換底/墨/點綴色,
// 坡度色階是語意(多陡),不隨主題變。

export interface ProfileTheme {
id: string;
label: string;
bg: string;
ink: string;
muted: string;
grid: string;
accent: string;
}

const THEMES: ProfileTheme[] = [
{ id: 'tiglet', label: 'Tiglet 暖橘', bg: '#FAF9F5', ink: '#1A1A18', muted: '#6B6A63', grid: '#D6D1C4', accent: '#D97757' },
{ id: 'tour', label: '環法黃', bg: '#FFFFFF', ink: '#14141E', muted: '#5A5A66', grid: '#E2E2E8', accent: '#D4A800' },
{ id: 'giro', label: '環義粉', bg: '#FFF6FA', ink: '#2B1A22', muted: '#8A6A78', grid: '#F0D4E2', accent: '#E5518D' },
{ id: 'vuelta', label: '環西紅', bg: '#FFFAF6', ink: '#241514', muted: '#8A6A62', grid: '#F0DCD0', accent: '#DA291C' },
];
// 出圖主題資料在 src/lib/profileThemes.ts(純資料抽成 lib,讓對比守門測試吃得到)

const W = 840;
const H = 380;
Expand Down
Loading