From 86013e535a5ad3649f9d344f6952a7075270a14f Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Wed, 27 May 2026 10:03:06 +0700 Subject: [PATCH] Add dark theme support --- index.html | 20 ++++++++++++++++++- src/app.js | 35 ++++++++++++++++++++++++++++++++ styles.css | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index b6b00fb..ebba474 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,14 @@ TaskForge — open-source task tracker + @@ -12,7 +20,16 @@

TaskForge

A tiny task list that lives in your browser.

-
+
+ +
Tasks are stored on this device. @@ -20,6 +37,7 @@

TaskForge

+
diff --git a/src/app.js b/src/app.js index 46dd724..463dadf 100644 --- a/src/app.js +++ b/src/app.js @@ -15,11 +15,14 @@ const authStatus = document.getElementById("auth-status"); const authAction = document.getElementById("auth-action"); const authAvatar = document.getElementById("auth-avatar"); const authMessage = document.getElementById("auth-message"); +const themeToggle = document.getElementById("theme-toggle"); +const THEME_KEY = "taskforge.theme"; let tasks = []; let user = null; async function init() { + initTheme(); const localTasks = loadLocal(); let completedSignIn = false; try { @@ -96,6 +99,11 @@ form.addEventListener("submit", async (e) => { render(); }); +themeToggle.addEventListener("click", () => { + const nextTheme = currentTheme() === "dark" ? "light" : "dark"; + applyTheme(nextTheme, true); +}); + authAction.addEventListener("click", async () => { authAction.disabled = true; try { @@ -147,3 +155,30 @@ function mergeTasks(localTasks, remoteTasks) { } init(); + +function initTheme() { + const saved = window.localStorage.getItem(THEME_KEY); + if (saved === "dark" || saved === "light") { + applyTheme(saved, false); + return; + } + applyTheme(preferredTheme(), false); +} + +function preferredTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +} + +function currentTheme() { + return document.documentElement.dataset.theme || preferredTheme(); +} + +function applyTheme(theme, persist) { + document.documentElement.dataset.theme = theme; + if (persist) window.localStorage.setItem(THEME_KEY, theme); + const dark = theme === "dark"; + themeToggle.setAttribute("aria-pressed", String(dark)); + themeToggle.textContent = dark ? "Light theme" : "Dark theme"; +} diff --git a/styles.css b/styles.css index a6c7afc..c1ece3b 100644 --- a/styles.css +++ b/styles.css @@ -1,4 +1,5 @@ :root { + color-scheme: light; --bg: #ffffff; --fg: #1f2328; --muted: #6e7781; @@ -6,9 +7,42 @@ --accent: #0969da; --accent-fg: #ffffff; --done: #8c959f; + --panel-bg: #f6f8fa; + --danger: #cf222e; + --danger-bg: rgba(207, 34, 46, 0.08); --radius: 6px; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + color-scheme: dark; + --bg: #0d1117; + --fg: #e6edf3; + --muted: #8b949e; + --border: #30363d; + --accent: #2f81f7; + --accent-fg: #ffffff; + --done: #6e7681; + --panel-bg: #161b22; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.16); + } +} + +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #0d1117; + --fg: #e6edf3; + --muted: #8b949e; + --border: #30363d; + --accent: #2f81f7; + --accent-fg: #ffffff; + --done: #6e7681; + --panel-bg: #161b22; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.16); +} + * { box-sizing: border-box; } @@ -43,6 +77,13 @@ body { font-size: 0.95rem; } +.header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; +} + .auth-panel { display: flex; align-items: center; @@ -64,6 +105,7 @@ body { overflow-wrap: anywhere; } +.theme-toggle, .auth-action { flex: 0 0 auto; padding: 0.45rem 0.75rem; @@ -75,6 +117,12 @@ body { cursor: pointer; } +.theme-toggle { + color: var(--fg); + background: var(--panel-bg); + border-color: var(--border); +} + .auth-action:disabled { cursor: wait; opacity: 0.7; @@ -171,8 +219,8 @@ body { } .task-delete:hover { - color: #cf222e; - background: rgba(207, 34, 46, 0.08); + color: var(--danger); + background: var(--danger-bg); } .empty-state { @@ -198,6 +246,12 @@ body { flex-direction: column; } + .header-actions { + align-items: stretch; + flex-direction: column; + width: 100%; + } + .auth-panel { justify-content: flex-start; min-width: 0;