From 6678dc218d7adbe96cd74314b24d30029a482ab3 Mon Sep 17 00:00:00 2001 From: VexCode24 Date: Mon, 25 May 2026 16:30:41 -0400 Subject: [PATCH] Add persistent dark theme toggle --- README.md | 2 +- index.html | 31 ++++++++++++++++++++-- src/app.js | 20 +++++++++++++++ styles.css | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 04e670c..5e32630 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 and dark themes - MIT licensed ## Quick start diff --git a/index.html b/index.html index 7309515..d26ac44 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,39 @@ 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..a3268cb 100644 --- a/src/app.js +++ b/src/app.js @@ -5,9 +5,24 @@ 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"); let tasks = load(); +function currentTheme() { + return document.documentElement.dataset.theme === "dark" ? "dark" : "light"; +} + +function setTheme(theme, persist = false) { + document.documentElement.dataset.theme = theme; + if (persist) { + window.localStorage.setItem("taskforge.theme", theme); + } + const nextTheme = theme === "dark" ? "light" : "dark"; + themeToggle.setAttribute("aria-label", `Switch to ${nextTheme} theme`); + themeToggle.setAttribute("aria-pressed", String(theme === "dark")); +} + function render() { list.innerHTML = ""; if (tasks.length === 0) { @@ -60,4 +75,9 @@ form.addEventListener("submit", (e) => { render(); }); +themeToggle.addEventListener("click", () => { + setTheme(currentTheme() === "dark" ? "light" : "dark", true); +}); + +setTheme(currentTheme()); render(); diff --git a/styles.css b/styles.css index 2fa1e18..6464ee6 100644 --- a/styles.css +++ b/styles.css @@ -6,7 +6,25 @@ --accent: #0969da; --accent-fg: #ffffff; --done: #8c959f; + --panel: #f6f8fa; + --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; + --panel: #161b22; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.14); + color-scheme: dark; } * { @@ -24,14 +42,23 @@ body { .app-header { padding: 2rem 1.25rem 1rem; - text-align: center; + background: var(--panel); 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; + letter-spacing: 0; } .tagline { @@ -40,6 +67,38 @@ body { font-size: 0.95rem; } +.theme-toggle { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 2.25rem; + padding: 0.45rem 0.7rem; + 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); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.theme-toggle-icon { + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-radius: 50%; + background: linear-gradient(90deg, currentColor 50%, transparent 50%); +} + .app-main { max-width: 36rem; margin: 0 auto; @@ -98,6 +157,7 @@ body { padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); + background: var(--panel); } .task-item.done .task-title { @@ -122,8 +182,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 +202,10 @@ body { .app-footer a { color: inherit; } + +@media (max-width: 34rem) { + .header-content { + align-items: flex-start; + flex-direction: column; + } +}