diff --git a/index.html b/index.html index b6b00fb..e3fdb78 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,26 @@ + TaskForge — open-source task tracker + @@ -12,14 +31,24 @@

TaskForge

A tiny task list that lives in your browser.

-
- - - Tasks are stored on this device. - - +
+ + + Tasks are stored on this device. + + +
diff --git a/src/app.js b/src/app.js index 46dd724..2828da0 100644 --- a/src/app.js +++ b/src/app.js @@ -15,6 +15,9 @@ 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 themePreference = window.matchMedia("(prefers-color-scheme: dark)"); +const themeStorageKey = "taskforge.theme"; let tasks = []; let user = null; @@ -114,6 +117,47 @@ authAction.addEventListener("click", async () => { } }); +themeToggle.addEventListener("click", () => { + const nextTheme = getTheme() === "dark" ? "light" : "dark"; + applyTheme(nextTheme); + try { + localStorage.setItem(themeStorageKey, nextTheme); + } catch { + // Keep the toggle usable even when persistence is unavailable. + } +}); + +themePreference.addEventListener("change", (event) => { + if (hasStoredTheme()) return; + applyTheme(event.matches ? "dark" : "light"); +}); + +function applyTheme(theme) { + document.documentElement.dataset.theme = theme; + renderThemeToggle(); +} + +function getTheme() { + return document.documentElement.dataset.theme === "dark" ? "dark" : "light"; +} + +function hasStoredTheme() { + try { + const savedTheme = localStorage.getItem(themeStorageKey); + return savedTheme === "dark" || savedTheme === "light"; + } catch { + return false; + } +} + +function renderThemeToggle() { + const isDark = getTheme() === "dark"; + const nextTheme = isDark ? "light" : "dark"; + themeToggle.textContent = isDark ? "Light mode" : "Dark mode"; + themeToggle.setAttribute("aria-pressed", String(isDark)); + themeToggle.setAttribute("aria-label", `Switch to ${nextTheme} theme`); +} + function renderAuth() { authAction.disabled = false; if (!user) { @@ -146,4 +190,5 @@ function mergeTasks(localTasks, remoteTasks) { return [...byId.values()].sort((a, b) => b.createdAt - a.createdAt); } +renderThemeToggle(); init(); diff --git a/styles.css b/styles.css index a6c7afc..ea4c311 100644 --- a/styles.css +++ b/styles.css @@ -1,14 +1,42 @@ :root { + color-scheme: light; --bg: #ffffff; + --surface: #f6f8fa; --fg: #1f2328; --muted: #6e7781; --border: #d0d7de; --accent: #0969da; --accent-fg: #ffffff; + --button-bg: #f6f8fa; + --button-hover: #eef2f6; --done: #8c959f; + --danger: #cf222e; + --danger-bg: rgba(207, 34, 46, 0.08); + --notice-bg: #fff8c5; + --notice-border: #d4a72c; + --notice-fg: #7d4e00; --radius: 6px; } +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #0d1117; + --surface: #161b22; + --fg: #f0f6fc; + --muted: #8b949e; + --border: #30363d; + --accent: #58a6ff; + --accent-fg: #0d1117; + --button-bg: #21262d; + --button-hover: #30363d; + --done: #8b949e; + --danger: #ff7b72; + --danger-bg: rgba(248, 81, 73, 0.12); + --notice-bg: #2d2300; + --notice-border: #9e6a03; + --notice-fg: #f2cc60; +} + * { box-sizing: border-box; } @@ -43,6 +71,13 @@ body { font-size: 0.95rem; } +.header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.6rem; +} + .auth-panel { display: flex; align-items: center; @@ -75,6 +110,22 @@ body { cursor: pointer; } +.theme-toggle { + flex: 0 0 auto; + padding: 0.45rem 0.75rem; + font: inherit; + color: var(--fg); + background: var(--button-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + cursor: pointer; +} + +.theme-toggle:hover { + background: var(--button-hover); + border-color: var(--accent); +} + .auth-action:disabled { cursor: wait; opacity: 0.7; @@ -89,9 +140,9 @@ body { .auth-message { margin: 0 0 1rem; padding: 0.6rem 0.75rem; - color: #7d4e00; - background: #fff8c5; - border: 1px solid #d4a72c; + color: var(--notice-fg); + background: var(--notice-bg); + border: 1px solid var(--notice-border); border-radius: var(--radius); } @@ -106,7 +157,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); } @@ -145,6 +196,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); } @@ -171,8 +223,8 @@ body { } .task-delete:hover { - color: #cf222e; - background: rgba(207, 34, 46, 0.08); + color: var(--danger); + background: var(--danger-bg); } .empty-state { @@ -204,6 +256,12 @@ body { width: 100%; } + .header-actions { + align-items: flex-start; + flex-direction: column; + width: 100%; + } + .auth-status { flex: 1; max-width: none;