From 2869b65a662f9846d7d68b8166b643779fb2ae36 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 26 May 2026 06:02:34 +0800 Subject: [PATCH] Add persisted dark theme support --- README.md | 2 +- eslint.config.mjs | 16 +++++++++++++ index.html | 28 +++++++++++++++++++--- src/app.js | 32 +++++++++++++++++++++++++ styles.css | 61 ++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 eslint.config.mjs diff --git a/README.md b/README.md index 04e670c..788ec49 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 persisted user preference - MIT licensed ## Quick start diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..78143f2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +export default [ + { + files: ["src/**/*.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + Blob: "readonly", + HTMLElement: "readonly", + URL: "readonly", + document: "readonly", + window: "readonly", + }, + }, + }, +]; diff --git a/index.html b/index.html index 7309515..ca003df 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,37 @@ - + 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..07659ac 100644 --- a/src/app.js +++ b/src/app.js @@ -5,9 +5,27 @@ 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 themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); +const themeStorageKey = "taskforge.theme"; let tasks = load(); +function getSystemTheme() { + return themeMedia.matches ? "dark" : "light"; +} + +function getSavedTheme() { + return localStorage.getItem(themeStorageKey); +} + +function applyTheme(theme) { + document.documentElement.dataset.theme = theme; + const isDark = theme === "dark"; + themeToggle.textContent = isDark ? "Light mode" : "Dark mode"; + themeToggle.setAttribute("aria-pressed", String(isDark)); +} + function render() { list.innerHTML = ""; if (tasks.length === 0) { @@ -49,6 +67,19 @@ function render() { } } +themeToggle.addEventListener("click", () => { + const nextTheme = + document.documentElement.dataset.theme === "dark" ? "light" : "dark"; + localStorage.setItem(themeStorageKey, nextTheme); + applyTheme(nextTheme); +}); + +themeMedia.addEventListener("change", () => { + if (!getSavedTheme()) { + applyTheme(getSystemTheme()); + } +}); + form.addEventListener("submit", (e) => { e.preventDefault(); const title = input.value.trim(); @@ -60,4 +91,5 @@ form.addEventListener("submit", (e) => { render(); }); +applyTheme(getSavedTheme() || getSystemTheme()); render(); diff --git a/styles.css b/styles.css index 2fa1e18..633bba5 100644 --- a/styles.css +++ b/styles.css @@ -6,7 +6,25 @@ --accent: #0969da; --accent-fg: #ffffff; --done: #8c959f; + --surface: #f6f8fa; + --danger: #cf222e; + --danger-bg: rgba(207, 34, 46, 0.08); --radius: 6px; + color-scheme: light; +} + +:root[data-theme="dark"] { + --bg: #0d1117; + --fg: #f0f6fc; + --muted: #8b949e; + --border: #30363d; + --accent: #58a6ff; + --accent-fg: #0d1117; + --done: #6e7681; + --surface: #161b22; + --danger: #ff7b72; + --danger-bg: rgba(255, 123, 114, 0.12); + color-scheme: dark; } * { @@ -15,8 +33,8 @@ 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; @@ -24,14 +42,22 @@ body { .app-header { padding: 2rem 1.25rem 1rem; - text-align: center; + background: var(--surface); border-bottom: 1px solid var(--border); } +.header-content { + max-width: 36rem; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + .app-header h1 { margin: 0; font-size: 1.75rem; - letter-spacing: -0.01em; } .tagline { @@ -78,6 +104,21 @@ body { cursor: pointer; } +.theme-toggle { + padding: 0.45rem 0.75rem; + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + cursor: pointer; + white-space: nowrap; +} + +.theme-toggle:hover { + border-color: var(--accent); +} + .task-form button:hover { filter: brightness(1.05); } @@ -98,6 +139,7 @@ body { padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); + background: var(--surface); } .task-item.done .task-title { @@ -122,8 +164,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 +184,10 @@ body { .app-footer a { color: inherit; } + +@media (max-width: 32rem) { + .header-content { + align-items: flex-start; + flex-direction: column; + } +}