From fe2f3fddf931561fe36ceac380aa754b7eb79abf Mon Sep 17 00:00:00 2001 From: TN0721 Date: Tue, 26 May 2026 12:34:43 +0800 Subject: [PATCH] Add persistent dark theme support --- README.md | 2 +- index.html | 25 +++++++++++++-- src/app.js | 36 +++++++++++++++++++++ styles.css | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 04e670c..2cceb93 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A simple, community-maintained open-source task tracker. Add tasks, check them o - Single-page, zero-build static web app - Tasks persist in `localStorage` — no account, no server, no tracking - Keyboard-friendly (more shortcuts coming, see #5) -- Light & dark themes (dark coming, see #1) +- Light & dark themes with a persistent user preference - MIT licensed ## Quick start diff --git a/index.html b/index.html index 7309515..be1e0ca 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,34 @@ - + + TaskForge — open-source task tracker +
-

TaskForge

-

A tiny task list that lives in your browser.

+
+

TaskForge

+

A tiny task list that lives in your browser.

+
+
diff --git a/src/app.js b/src/app.js index fe0e9c1..5cc1b6a 100644 --- a/src/app.js +++ b/src/app.js @@ -5,9 +5,34 @@ const form = document.getElementById("task-form"); const input = document.getElementById("task-input"); const list = document.getElementById("task-list"); const emptyState = document.getElementById("empty-state"); +const themeToggle = document.getElementById("theme-toggle"); +const themePreference = window.matchMedia("(prefers-color-scheme: dark)"); +const THEME_KEY = "taskforge.theme"; let tasks = load(); +function getStoredTheme() { + const theme = window.localStorage.getItem(THEME_KEY); + return theme === "dark" || theme === "light" ? theme : null; +} + +function getActiveTheme() { + return getStoredTheme() ?? (themePreference.matches ? "dark" : "light"); +} + +function updateThemeToggle() { + const activeTheme = getActiveTheme(); + const isDark = activeTheme === "dark"; + themeToggle.textContent = isDark ? "Light mode" : "Dark mode"; + themeToggle.setAttribute("aria-pressed", String(isDark)); +} + +function setTheme(theme) { + window.localStorage.setItem(THEME_KEY, theme); + document.documentElement.dataset.theme = theme; + updateThemeToggle(); +} + function render() { list.innerHTML = ""; if (tasks.length === 0) { @@ -60,4 +85,15 @@ form.addEventListener("submit", (e) => { render(); }); +themeToggle.addEventListener("click", () => { + setTheme(getActiveTheme() === "dark" ? "light" : "dark"); +}); + +themePreference.addEventListener("change", () => { + if (!getStoredTheme()) { + updateThemeToggle(); + } +}); + +updateThemeToggle(); render(); diff --git a/styles.css b/styles.css index 2fa1e18..3925eda 100644 --- a/styles.css +++ b/styles.css @@ -1,12 +1,49 @@ :root { --bg: #ffffff; + --surface: #ffffff; --fg: #1f2328; --muted: #6e7781; --border: #d0d7de; --accent: #0969da; --accent-fg: #ffffff; --done: #8c959f; + --danger: #cf222e; + --danger-bg: rgba(207, 34, 46, 0.08); + --shadow: rgba(31, 35, 40, 0.08); --radius: 6px; + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #0d1117; + --surface: #161b22; + --fg: #e6edf3; + --muted: #8b949e; + --border: #30363d; + --accent: #2f81f7; + --accent-fg: #ffffff; + --done: #6e7681; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.12); + --shadow: rgba(1, 4, 9, 0.3); + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + --bg: #0d1117; + --surface: #161b22; + --fg: #e6edf3; + --muted: #8b949e; + --border: #30363d; + --accent: #2f81f7; + --accent-fg: #ffffff; + --done: #6e7681; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.12); + --shadow: rgba(1, 4, 9, 0.3); + color-scheme: dark; } * { @@ -15,19 +52,27 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, - sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.5; } .app-header { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; padding: 2rem 1.25rem 1rem; text-align: center; border-bottom: 1px solid var(--border); } +.header-copy { + min-width: 0; +} + .app-header h1 { margin: 0; font-size: 1.75rem; @@ -40,6 +85,29 @@ body { font-size: 0.95rem; } +.theme-toggle { + position: absolute; + right: 1.25rem; + padding: 0.45rem 0.7rem; + font: inherit; + font-size: 0.85rem; + color: var(--fg); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 1px 2px var(--shadow); + cursor: pointer; +} + +.theme-toggle:hover { + border-color: var(--accent); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + .app-main { max-width: 36rem; margin: 0 auto; @@ -57,7 +125,7 @@ body { padding: 0.5rem 0.75rem; font: inherit; color: inherit; - background: var(--bg); + background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); } @@ -96,6 +164,7 @@ body { align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; + background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); } @@ -122,8 +191,8 @@ body { } .task-delete:hover { - color: #cf222e; - background: rgba(207, 34, 46, 0.08); + color: var(--danger); + background: var(--danger-bg); } .empty-state { @@ -142,3 +211,15 @@ body { .app-footer a { color: inherit; } + +@media (max-width: 40rem) { + .app-header { + flex-direction: column; + align-items: stretch; + } + + .theme-toggle { + position: static; + align-self: center; + } +}