From 6d715dca093f9d7f3ab27aaa34fdb93d101934d0 Mon Sep 17 00:00:00 2001 From: nguyenduc071912 Date: Wed, 27 May 2026 10:19:53 +0700 Subject: [PATCH] Add dark theme support --- index.html | 18 ++++++++++++++++++ src/app.js | 3 +++ src/theme.js | 38 ++++++++++++++++++++++++++++++++++++++ styles.css | 47 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/theme.js diff --git a/index.html b/index.html index b6b00fb..6b2f3fd 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,16 @@ TaskForge — open-source task tracker + @@ -13,6 +23,14 @@

TaskForge

A tiny task list that lives in your browser.

+ Tasks are stored on this device. diff --git a/src/app.js b/src/app.js index 46dd724..2170df1 100644 --- a/src/app.js +++ b/src/app.js @@ -6,6 +6,7 @@ import { } from "./auth.js"; import { load, loadLocal, save } from "./storage.js"; import { createTask, toggleTask, removeTask } from "./tasks.js"; +import { initTheme } from "./theme.js"; const form = document.getElementById("task-form"); const input = document.getElementById("task-input"); @@ -15,11 +16,13 @@ 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"); let tasks = []; let user = null; async function init() { + initTheme(themeToggle); const localTasks = loadLocal(); let completedSignIn = false; try { diff --git a/src/theme.js b/src/theme.js new file mode 100644 index 0000000..9430cfc --- /dev/null +++ b/src/theme.js @@ -0,0 +1,38 @@ +const THEME_KEY = "taskforge.theme"; +const DARK_QUERY = "(prefers-color-scheme: dark)"; + +export function initTheme(toggleButton) { + const mediaQuery = window.matchMedia?.(DARK_QUERY); + + applyTheme(getPreferredTheme(mediaQuery), toggleButton); + + toggleButton?.addEventListener("click", () => { + const nextTheme = currentTheme() === "dark" ? "light" : "dark"; + window.localStorage.setItem(THEME_KEY, nextTheme); + applyTheme(nextTheme, toggleButton); + }); + + mediaQuery?.addEventListener("change", (event) => { + if (window.localStorage.getItem(THEME_KEY)) return; + applyTheme(event.matches ? "dark" : "light", toggleButton); + }); +} + +function getPreferredTheme(mediaQuery) { + const storedTheme = window.localStorage.getItem(THEME_KEY); + if (storedTheme === "dark" || storedTheme === "light") return storedTheme; + return mediaQuery?.matches ? "dark" : "light"; +} + +function currentTheme() { + return document.documentElement.dataset.theme === "dark" ? "dark" : "light"; +} + +function applyTheme(theme, toggleButton) { + document.documentElement.dataset.theme = theme; + if (!toggleButton) return; + + const nextTheme = theme === "dark" ? "light" : "dark"; + toggleButton.textContent = nextTheme === "dark" ? "Dark" : "Light"; + toggleButton.setAttribute("aria-label", `Switch to ${nextTheme} theme`); +} diff --git a/styles.css b/styles.css index a6c7afc..cba4d9a 100644 --- a/styles.css +++ b/styles.css @@ -6,7 +6,29 @@ --accent: #0969da; --accent-fg: #ffffff; --done: #8c959f; + --message-bg: #fff8c5; + --message-border: #d4a72c; + --message-fg: #7d4e00; + --danger: #cf222e; + --danger-bg: rgba(207, 34, 46, 0.08); --radius: 6px; + color-scheme: light; +} + +:root[data-theme="dark"] { + --bg: #0d1117; + --fg: #e6edf3; + --muted: #8b949e; + --border: #30363d; + --accent: #2f81f7; + --accent-fg: #ffffff; + --done: #6e7681; + --message-bg: #332701; + --message-border: #9e6a03; + --message-fg: #f0c36a; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.14); + color-scheme: dark; } * { @@ -80,6 +102,21 @@ body { opacity: 0.7; } +.theme-toggle { + flex: 0 0 auto; + padding: 0.45rem 0.75rem; + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + cursor: pointer; +} + +.theme-toggle:hover { + border-color: var(--accent); +} + .app-main { max-width: 36rem; margin: 0 auto; @@ -89,9 +126,9 @@ body { .auth-message { margin: 0 0 1rem; padding: 0.6rem 0.75rem; - color: #7d4e00; - background: #fff8c5; - border: 1px solid #d4a72c; + color: var(--message-fg); + background: var(--message-bg); + border: 1px solid var(--message-border); border-radius: var(--radius); } @@ -171,8 +208,8 @@ body { } .task-delete:hover { - color: #cf222e; - background: rgba(207, 34, 46, 0.08); + color: var(--danger); + background: var(--danger-bg); } .empty-state {