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 {