diff --git a/index.html b/index.html
index b6b00fb..ebba474 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,14 @@
TaskForge — open-source task tracker
+
@@ -12,7 +20,16 @@
TaskForge
A tiny task list that lives in your browser.
-
+
diff --git a/src/app.js b/src/app.js
index 46dd724..463dadf 100644
--- a/src/app.js
+++ b/src/app.js
@@ -15,11 +15,14 @@ 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");
+const THEME_KEY = "taskforge.theme";
let tasks = [];
let user = null;
async function init() {
+ initTheme();
const localTasks = loadLocal();
let completedSignIn = false;
try {
@@ -96,6 +99,11 @@ form.addEventListener("submit", async (e) => {
render();
});
+themeToggle.addEventListener("click", () => {
+ const nextTheme = currentTheme() === "dark" ? "light" : "dark";
+ applyTheme(nextTheme, true);
+});
+
authAction.addEventListener("click", async () => {
authAction.disabled = true;
try {
@@ -147,3 +155,30 @@ function mergeTasks(localTasks, remoteTasks) {
}
init();
+
+function initTheme() {
+ const saved = window.localStorage.getItem(THEME_KEY);
+ if (saved === "dark" || saved === "light") {
+ applyTheme(saved, false);
+ return;
+ }
+ applyTheme(preferredTheme(), false);
+}
+
+function preferredTheme() {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+}
+
+function currentTheme() {
+ return document.documentElement.dataset.theme || preferredTheme();
+}
+
+function applyTheme(theme, persist) {
+ document.documentElement.dataset.theme = theme;
+ if (persist) window.localStorage.setItem(THEME_KEY, theme);
+ const dark = theme === "dark";
+ themeToggle.setAttribute("aria-pressed", String(dark));
+ themeToggle.textContent = dark ? "Light theme" : "Dark theme";
+}
diff --git a/styles.css b/styles.css
index a6c7afc..c1ece3b 100644
--- a/styles.css
+++ b/styles.css
@@ -1,4 +1,5 @@
:root {
+ color-scheme: light;
--bg: #ffffff;
--fg: #1f2328;
--muted: #6e7781;
@@ -6,9 +7,42 @@
--accent: #0969da;
--accent-fg: #ffffff;
--done: #8c959f;
+ --panel-bg: #f6f8fa;
+ --danger: #cf222e;
+ --danger-bg: rgba(207, 34, 46, 0.08);
--radius: 6px;
}
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --fg: #e6edf3;
+ --muted: #8b949e;
+ --border: #30363d;
+ --accent: #2f81f7;
+ --accent-fg: #ffffff;
+ --done: #6e7681;
+ --panel-bg: #161b22;
+ --danger: #ff7b72;
+ --danger-bg: rgba(248, 81, 73, 0.16);
+ }
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --fg: #e6edf3;
+ --muted: #8b949e;
+ --border: #30363d;
+ --accent: #2f81f7;
+ --accent-fg: #ffffff;
+ --done: #6e7681;
+ --panel-bg: #161b22;
+ --danger: #ff7b72;
+ --danger-bg: rgba(248, 81, 73, 0.16);
+}
+
* {
box-sizing: border-box;
}
@@ -43,6 +77,13 @@ body {
font-size: 0.95rem;
}
+.header-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
.auth-panel {
display: flex;
align-items: center;
@@ -64,6 +105,7 @@ body {
overflow-wrap: anywhere;
}
+.theme-toggle,
.auth-action {
flex: 0 0 auto;
padding: 0.45rem 0.75rem;
@@ -75,6 +117,12 @@ body {
cursor: pointer;
}
+.theme-toggle {
+ color: var(--fg);
+ background: var(--panel-bg);
+ border-color: var(--border);
+}
+
.auth-action:disabled {
cursor: wait;
opacity: 0.7;
@@ -171,8 +219,8 @@ body {
}
.task-delete:hover {
- color: #cf222e;
- background: rgba(207, 34, 46, 0.08);
+ color: var(--danger);
+ background: var(--danger-bg);
}
.empty-state {
@@ -198,6 +246,12 @@ body {
flex-direction: column;
}
+ .header-actions {
+ align-items: stretch;
+ flex-direction: column;
+ width: 100%;
+ }
+
.auth-panel {
justify-content: flex-start;
min-width: 0;