Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bed22cd
Merge pull request #83 from alvin000009238/main
alvin000009238 Mar 31, 2026
a2ef854
feat: add accessible light theme toggle
alvin000009238 Mar 31, 2026
8043951
Merge pull request #84 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
615b603
fix: prevent header action text clipping in light theme
alvin000009238 Mar 31, 2026
b5e66f6
Merge branch 'dev' into codex/add-light-mode-and-theme-toggle-harb4d
alvin000009238 Mar 31, 2026
8ec25fa
Merge pull request #85 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
61ddf8f
fix: compact header actions to reduce space usage
alvin000009238 Mar 31, 2026
ea82ecd
Merge branch 'dev' into codex/add-light-mode-and-theme-toggle-ggawno
alvin000009238 Mar 31, 2026
176f982
Merge pull request #86 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
a8bd092
fix: improve chart text contrast across theme changes
alvin000009238 Mar 31, 2026
1a786e6
Merge branch 'dev' into codex/add-light-mode-and-theme-toggle-zya5rs
alvin000009238 Mar 31, 2026
d49917d
Merge pull request #87 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
5c2e031
fix: use manual dark default theme and md3 toggle icons
alvin000009238 Mar 31, 2026
285e939
Merge branch 'dev' into codex/add-light-mode-and-theme-toggle-p94oog
alvin000009238 Mar 31, 2026
a288985
Merge pull request #88 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
45abbf6
fix: swap theme toggle to md3-style svg icons
alvin000009238 Mar 31, 2026
b666c85
Merge branch 'dev' into codex/add-light-mode-and-theme-toggle-efl5ta
alvin000009238 Mar 31, 2026
52bc78a
Merge pull request #89 from alvin000009238/codex/add-light-mode-and-t…
alvin000009238 Mar 31, 2026
2e9d751
Update light and dark mode toggle SVG icons
alvin000009238 Mar 31, 2026
aa30858
Merge pull request #90 from alvin000009238/codex/replace-light-and-da…
alvin000009238 Mar 31, 2026
974f03f
Fix typo in heading from 'vs' to 'vs.'
alvin000009238 Mar 31, 2026
5099d53
Fix theme init CSP and improve theme/chart test coverage
alvin000009238 Mar 31, 2026
ede308a
Merge pull request #92 from alvin000009238/codex/fix-csp-issue-with-i…
alvin000009238 Mar 31, 2026
4f84465
Fix chart palette staleness and stabilize theme tests
alvin000009238 Mar 31, 2026
007dfab
Merge branch 'dev' into codex/fix-csp-issue-with-inline-theme-init-sc…
alvin000009238 Mar 31, 2026
6f9ee92
Merge pull request #93 from alvin000009238/codex/fix-csp-issue-with-i…
alvin000009238 Mar 31, 2026
d36cb9b
Harden early theme init against storage access failures
alvin000009238 Mar 31, 2026
1c75fec
Merge branch 'dev' into codex/fix-csp-issue-with-inline-theme-init-sc…
alvin000009238 Mar 31, 2026
8c9a35f
Merge pull request #95 from alvin000009238/codex/fix-csp-issue-with-i…
alvin000009238 Mar 31, 2026
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
122 changes: 104 additions & 18 deletions frontend/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,31 @@ let radarChartInstance = null;
let barChartInstance = null;
let chartJsLoadPromise = null;
let latestRenderToken = 0;
let cachedChartPalette = null;

if (typeof document !== 'undefined') {
document.addEventListener('themechange', () => {
cachedChartPalette = null;
applyThemeToExistingCharts();
});
}

export function generateCharts(subjects) {
if (!Array.isArray(subjects) || subjects.length === 0) return;

const labels = subjects.map((subject) => shortenName(subject.SubjectName));
const myScores = subjects.map((subject) => subject.scoreValue ?? getNumericScore(subject.ScoreDisplay, subject.Score));
const avgScores = subjects.map((subject) => subject.classAvgValue ?? getNumericScore(subject.ClassAVGScoreDisplay, subject.ClassAVGScore));
const palette = getChartThemePalette();

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateCharts() 內的 const palette = getChartThemePalette(); 目前沒有被使用,反而每次呼叫都會額外觸發一次 getComputedStyle()(成本不低)。建議移除這個未使用的變數,或如果原意是要在載入 Chart.js 前先快照當下主題,請改成實際使用(例如初始化 cachedChartPalette)。

Suggested change
const palette = getChartThemePalette();

Copilot uses AI. Check for mistakes.
const renderToken = ++latestRenderToken;

ensureChartJsLoaded()
.then((ChartCtor) => {
if (renderToken !== latestRenderToken) return;
updateRadarChart(ChartCtor, labels, myScores, avgScores);
updateBarChart(ChartCtor, labels, myScores, avgScores);

const palette = getCachedChartThemePalette();
updateRadarChart(ChartCtor, labels, myScores, avgScores, palette);
updateBarChart(ChartCtor, labels, myScores, avgScores, palette);
})
.catch((error) => {
console.warn('Failed to load Chart.js', error);
Expand Down Expand Up @@ -98,7 +109,7 @@ function ensureChartJsLoaded() {
return chartJsLoadPromise;
}

function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
function updateRadarChart(ChartCtor, labels, myScores, avgScores, palette) {
const radarCanvas = document.getElementById('radarChart');
const radarCtx = radarCanvas?.getContext('2d');
if (!radarCtx) return;
Expand All @@ -116,8 +127,8 @@ function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
borderColor: '#6366f1',
borderWidth: 2,
pointBackgroundColor: '#6366f1',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointBorderColor: palette.surface,
pointHoverBackgroundColor: palette.surface,
pointHoverBorderColor: '#6366f1'
},
{
Expand All @@ -127,8 +138,8 @@ function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
borderColor: '#10b981',
borderWidth: 2,
pointBackgroundColor: '#10b981',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointBorderColor: palette.surface,
pointHoverBackgroundColor: palette.surface,
pointHoverBorderColor: '#10b981'
}
]
Expand All @@ -142,17 +153,17 @@ function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
max: 100,
ticks: {
stepSize: 20,
color: '#94a3b8',
color: palette.textMuted,
backdropColor: 'transparent'
},
grid: {
color: 'rgba(148, 163, 184, 0.2)'
color: palette.grid
},
angleLines: {
color: 'rgba(148, 163, 184, 0.2)'
color: palette.grid
},
pointLabels: {
color: '#f8fafc',
color: palette.textSecondary,
font: {
size: 12
}
Expand All @@ -163,7 +174,7 @@ function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
legend: {
position: 'bottom',
labels: {
color: '#f8fafc',
color: palette.textMain,
padding: 20,
font: {
size: 13
Expand All @@ -182,7 +193,7 @@ function updateRadarChart(ChartCtor, labels, myScores, avgScores) {
radarChartInstance.update('none');
}

function updateBarChart(ChartCtor, labels, myScores, avgScores) {
function updateBarChart(ChartCtor, labels, myScores, avgScores, palette) {
const barCanvas = document.getElementById('barChart');
const barCtx = barCanvas?.getContext('2d');
if (!barCtx) return;
Expand Down Expand Up @@ -217,28 +228,28 @@ function updateBarChart(ChartCtor, labels, myScores, avgScores) {
scales: {
x: {
grid: {
color: 'rgba(148, 163, 184, 0.1)'
color: palette.gridSubtle
},
ticks: {
color: '#94a3b8'
color: palette.textMuted
}
},
y: {
beginAtZero: true,
max: 100,
grid: {
color: 'rgba(148, 163, 184, 0.1)'
color: palette.gridSubtle
},
ticks: {
color: '#94a3b8'
color: palette.textMuted
}
}
},
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#f8fafc',
color: palette.textMain,
padding: 20,
font: {
size: 13
Expand All @@ -257,6 +268,81 @@ function updateBarChart(ChartCtor, labels, myScores, avgScores) {
barChartInstance.update('none');
}

function getChartThemePalette() {
const styles = getComputedStyle(document.documentElement);
const readVar = (name, fallback) => {
const value = styles.getPropertyValue(name).trim();
return value || fallback;
};

return {
textMain: readVar('--color-text-main', '#0f172a'),
textSecondary: readVar('--color-text-secondary', '#334155'),
textMuted: readVar('--color-text-muted', '#64748b'),
surface: readVar('--color-surface-elevated', '#ffffff'),
grid: readVar('--color-border-subtle', 'rgba(148, 163, 184, 0.22)'),
gridSubtle: readVar('--color-border-extra-subtle', 'rgba(148, 163, 184, 0.16)')
};
}

function getCachedChartThemePalette() {
if (!cachedChartPalette) {
cachedChartPalette = getChartThemePalette();
}
return cachedChartPalette;
}

function applyThemeToExistingCharts() {
const palette = getCachedChartThemePalette();

if (radarChartInstance) {
const radarOptions = radarChartInstance.options;
const radarScale = radarOptions.scales?.r;
const radarLegendLabels = radarOptions.plugins?.legend?.labels;
if (radarScale) {
radarScale.ticks.color = palette.textMuted;
radarScale.grid.color = palette.grid;
radarScale.angleLines.color = palette.grid;
radarScale.pointLabels.color = palette.textSecondary;
}
if (radarLegendLabels) {
radarLegendLabels.color = palette.textMain;
}

radarChartInstance.data.datasets.forEach((dataset) => {
dataset.pointBorderColor = palette.surface;
dataset.pointHoverBackgroundColor = palette.surface;
});

radarChartInstance.update('none');
}

if (barChartInstance) {
const barOptions = barChartInstance.options;
const xScale = barOptions.scales?.x;
const yScale = barOptions.scales?.y;
const barLegendLabels = barOptions.plugins?.legend?.labels;
if (xScale) {
xScale.grid.color = palette.gridSubtle;
xScale.ticks.color = palette.textMuted;
}
if (yScale) {
yScale.grid.color = palette.gridSubtle;
yScale.ticks.color = palette.textMuted;
}
if (barLegendLabels) {
barLegendLabels.color = palette.textMain;
}

barChartInstance.update('none');
}
}

export function __setChartInstancesForTest(instances = {}) {
if (Object.hasOwn(instances, 'radar')) radarChartInstance = instances.radar;
if (Object.hasOwn(instances, 'bar')) barChartInstance = instances.bar;
}

Comment on lines +341 to +345

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__setChartInstancesForTest 目前會被打包成正式輸出 API,且專案其他模組看起來沒有類似的 test-only export 慣例。建議將此測試注入點改為在測試環境才可用(例如用條件式匯出/掛到全域測試鉤子/或把 chart instance 存取抽到可注入的依賴),避免未來被誤用成公開介面而增加維護負擔。

Suggested change
export function __setChartInstancesForTest(instances = {}) {
if (Object.hasOwn(instances, 'radar')) radarChartInstance = instances.radar;
if (Object.hasOwn(instances, 'bar')) barChartInstance = instances.bar;
}
function __setChartInstancesForTest(instances = {}) {
if (Object.hasOwn(instances, 'radar')) radarChartInstance = instances.radar;
if (Object.hasOwn(instances, 'bar')) barChartInstance = instances.bar;
}
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'test') {
// Expose test-only hook via a global in test environments without making it a public export.
// This avoids leaking a test helper into the formal production API surface.
// eslint-disable-next-line no-undef
globalThis.__setChartInstancesForTest = __setChartInstancesForTest;
}

Copilot uses AI. Check for mistakes.
function clearCanvas(canvasId) {
const canvas = document.getElementById(canvasId);
const context = canvas?.getContext?.('2d');
Expand Down
2 changes: 2 additions & 0 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import './style.css';
import { checkDisclaimer, loadGradesData } from './storage.js';
import { setupThemeToggle } from './theme.js';

// ── Lazy init 狀態 ──────────────────────
let syncInited = false;
Expand All @@ -27,6 +28,7 @@ export async function ensureShareReady() {

// ── 首屏必要初始化 ──────────────────────
document.addEventListener('DOMContentLoaded', () => {
setupThemeToggle();
checkDisclaimer();
loadGradesData();

Expand Down
17 changes: 12 additions & 5 deletions frontend/styles/header.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

padding-right: 24px;

background: rgba(28, 28, 30, 0.72);
background: var(--color-header-bg);

backdrop-filter: blur(20px) saturate(1.8);

Expand Down Expand Up @@ -154,6 +154,11 @@

}

.header-actions {
justify-content: flex-end;
flex-wrap: nowrap;
}



.time-info {
Expand Down Expand Up @@ -238,7 +243,7 @@

height: 40px;

padding: 0 24px;
padding: 0 18px;

background: var(--color-primary);

Expand All @@ -248,7 +253,7 @@

color: var(--md-sys-color-on-primary);

font-size: 14px;
font-size: 13px;

font-weight: 500;

Expand All @@ -260,6 +265,10 @@

touch-action: manipulation;

white-space: nowrap;

flex-shrink: 0;

}


Expand Down Expand Up @@ -476,5 +485,3 @@ visibility: visible;

}



4 changes: 2 additions & 2 deletions frontend/styles/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

inset: 0;

background: rgba(0, 0, 0, 0.45);
background: var(--color-modal-overlay);

backdrop-filter: blur(10px);

Expand Down Expand Up @@ -382,7 +382,7 @@ visibility: visible;

border-color: var(--color-primary);

box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2);
box-shadow: 0 0 0 3px var(--color-focus-ring);

}

Expand Down
2 changes: 1 addition & 1 deletion frontend/styles/motion.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@

::selection {

background: rgba(0, 122, 255, 0.25);
background: var(--color-selection);

}

Expand Down
12 changes: 6 additions & 6 deletions frontend/styles/onboarding.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

inset: 0;

background: rgba(7, 10, 18, 0.68);
background: var(--color-tour-overlay);

}

Expand All @@ -30,17 +30,17 @@

position: fixed;

border: 2px solid rgba(168, 199, 250, 0.95);
border: 2px solid var(--color-tour-highlight);

border-radius: 12px;

box-shadow:

0 0 0 9999px rgba(7, 10, 18, 0.68),
0 0 0 9999px var(--color-tour-overlay),

0 0 0 1px rgba(255, 255, 255, 0.15) inset,
0 0 0 1px var(--color-tour-highlight-inset) inset,

0 0 24px rgba(168, 199, 250, 0.45);
0 0 24px var(--color-tour-highlight-glow);

transition: all 0.2s var(--ease);

Expand Down Expand Up @@ -164,7 +164,7 @@

z-index: 1550;

background: rgba(7, 10, 18, 0.68);
background: var(--color-tour-overlay);

display: flex;

Expand Down
7 changes: 5 additions & 2 deletions frontend/styles/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@

}

.header-actions {
width: auto;
justify-content: flex-end;
}



.logo-text h1 {
Expand Down Expand Up @@ -190,5 +195,3 @@

}



Loading
Loading