Add the Nord theme and make it the default
The theme picker shows NORD, MOCHA and LATTE. Nord is the base theme in the CSS, the browser bar color and the PWA manifest. The startup script applies the theme before the first paint. A device that saved Mocha changes to Nord one time. After that, a user can select Mocha again and the app keeps it. The Nord red #bf616a has only 3.05:1 contrast on the background, so the danger color is the lighter #dc8890 (4.74:1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9fba5de6d7
commit
c90c0ddf9d
7 files changed
+125
-13
No files matched your search
+11
-4
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#1e1e2e" />
|
||||
<meta name="theme-color" content="#2e3440" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="description" content="A quiet place to log your time." />
|
||||
@@ -12,10 +12,17 @@
|
||||
<link rel="apple-touch-icon" href="/icon-180.png" />
|
||||
<script>
|
||||
try {
|
||||
if (localStorage.getItem("timelog-theme") === "latte") {
|
||||
document.documentElement.dataset.theme = "latte";
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute("content", "#e6e9ef");
|
||||
// Keep these colors the same as THEME_COLORS in src/client/theme.ts.
|
||||
var colors = { nord: "#2e3440", mocha: "#1e1e2e", latte: "#e6e9ef" };
|
||||
var theme = localStorage.getItem("timelog-theme");
|
||||
// Nord replaced Mocha as the default. Change a saved Mocha to Nord one time only.
|
||||
if (!localStorage.getItem("timelog-theme-nord")) {
|
||||
if (theme === "mocha") localStorage.setItem("timelog-theme", (theme = "nord"));
|
||||
localStorage.setItem("timelog-theme-nord", "1");
|
||||
}
|
||||
if (!colors.hasOwnProperty(theme)) theme = "nord";
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute("content", colors[theme]);
|
||||
} catch {}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.css?v=__BUILD_ID__" />
|
||||
|
||||
+6
-5
@@ -6,15 +6,16 @@ import { dueMs, recordEnd, type TimeRecord } from "../shared/record.ts";
|
||||
import { normalizeTag } from "../shared/tag.ts";
|
||||
import { countdown, pad, pageTitle } from "./title.ts";
|
||||
import { readyToReload } from "./update.ts";
|
||||
import { THEMES, THEME_COLORS, isTheme, type Theme } from "./theme.ts";
|
||||
|
||||
type View = "timer" | "log" | "insights" | "settings";
|
||||
type Theme = "latte" | "mocha";
|
||||
type Me = { authenticated: boolean; dev: boolean; githubConfigured: boolean; vapidPublicKey: string | null };
|
||||
|
||||
const app = document.getElementById("app")!;
|
||||
let records: TimeRecord[] = [];
|
||||
let view: View = "timer";
|
||||
let theme: Theme = document.documentElement.dataset.theme === "latte" ? "latte" : "mocha";
|
||||
const startTheme = document.documentElement.dataset.theme;
|
||||
let theme: Theme = isTheme(startTheme) ? startTheme : "nord";
|
||||
let me: Me | null = null;
|
||||
let syncStatus: SyncStatus = navigator.onLine ? "pending" : "offline";
|
||||
let selectedTag = normalizeTag(localStorage.getItem("timelog-last-tag") || "Study");
|
||||
@@ -65,7 +66,7 @@ function markSignedOut() {
|
||||
function setTheme(next: Theme) {
|
||||
theme = next;
|
||||
document.documentElement.dataset.theme = next;
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", next === "latte" ? "#e6e9ef" : "#1e1e2e");
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", THEME_COLORS[next]);
|
||||
localStorage.setItem("timelog-theme", next);
|
||||
render();
|
||||
}
|
||||
@@ -232,7 +233,7 @@ function settingsPage() {
|
||||
const permission = "Notification" in window ? Notification.permission : "unsupported";
|
||||
return `<section class="page">${pageHeading("Preferences.", "KEEP IT SIMPLE", "// SYSTEM")}
|
||||
<div class="panel"><div class="panel-inner"><div class="panel-label"><span>// DATA & DEVICE</span><span class="end">PRIVATE</span></div>
|
||||
<div class="settings-row"><div><h3>Appearance</h3><p>Catppuccin colors. Saved on this device.</p></div><div class="theme-toggle" role="group" aria-label="Color theme"><button class="theme-option ${theme === "latte" ? "active" : ""}" data-action="theme" data-theme="latte" aria-pressed="${theme === "latte"}" type="button">LATTE</button><button class="theme-option ${theme === "mocha" ? "active" : ""}" data-action="theme" data-theme="mocha" aria-pressed="${theme === "mocha"}" type="button">MOCHA</button></div></div>
|
||||
<div class="settings-row"><div><h3>Appearance</h3><p>Nord and Catppuccin colors. Saved on this device.</p></div><div class="theme-toggle" role="group" aria-label="Color theme">${THEMES.map(option => `<button class="theme-option ${theme === option ? "active" : ""}" data-action="theme" data-theme="${option}" aria-pressed="${theme === option}" type="button">${option.toUpperCase()}</button>`).join("")}</div></div>
|
||||
<div class="settings-row"><div><h3>Forest CSV import</h3><p>Bring in your existing sessions. Re-importing the same file skips duplicates.</p></div><button class="small-btn" data-action="import" type="button">IMPORT CSV</button><input class="hidden-input" id="csv-file" type="file" accept=".csv,text/csv" /></div>
|
||||
<div class="settings-row"><div><h3>Forest CSV export</h3><p>Download your completed sessions with Forest's six-column layout.</p></div><button class="small-btn" data-action="export" type="button">EXPORT CSV</button></div>
|
||||
<div class="settings-row"><div><h3>Completion notifications</h3><p>${permission === "denied" ? "Blocked in your device settings." : !canNotify ? "On iPhone, add Timelog to your Home Screen, then open it there to enable alerts." : permission === "granted" ? "Permission granted on this device. Reconnect if alerts stop arriving." : "Get an alert when a session reaches its duration. On iPhone, open Timelog from your Home Screen first."}</p></div><button class="small-btn" data-action="notify" type="button" ${!canNotify || !pushRegistration || permission === "denied" ? "disabled" : ""}>${permission === "granted" ? "RECONNECT" : "ENABLE"}</button></div>
|
||||
@@ -428,7 +429,7 @@ async function dispatch(action: string, element: HTMLElement) {
|
||||
case "prev-insight": insightOffset--; render(); break;
|
||||
case "next-insight": insightOffset++; render(); break;
|
||||
case "range": insightKind = element.dataset.range as RangeKind; insightOffset = 0; render(); break;
|
||||
case "theme": setTheme(element.dataset.theme === "latte" ? "latte" : "mocha"); break;
|
||||
case "theme": setTheme(isTheme(element.dataset.theme) ? element.dataset.theme : "nord"); break;
|
||||
case "add": editingId = "new"; render(); break;
|
||||
case "edit": editingId = element.dataset.id || null; render(); break;
|
||||
case "close-modal": editingId = null; render(); break;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1e1e2e",
|
||||
"theme_color": "#1e1e2e",
|
||||
"background_color": "#2e3440",
|
||||
"theme_color": "#2e3440",
|
||||
"icons": [
|
||||
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" },
|
||||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
|
||||
+22
-1
@@ -1,4 +1,26 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #2e3440;
|
||||
--panel: #272c36;
|
||||
--panel-soft: #3b4252;
|
||||
--field-bg: #242933;
|
||||
--ink: #eceff4;
|
||||
--muted: #d8dee9;
|
||||
--faint: #b6bfcd;
|
||||
--line: #616e88;
|
||||
--line-bright: #7b88a1;
|
||||
--accent: #88c0d0;
|
||||
--accent-ink: #2e3440;
|
||||
--chart: #8fbcbb;
|
||||
--good: #a3be8c;
|
||||
/* Nord red #bf616a, made lighter for 4.5:1 text contrast. */
|
||||
--danger: #dc8890;
|
||||
--scrim: #242933cc;
|
||||
--shadow: #24293399;
|
||||
--font: "SFMono-Regular", "Cascadia Code", "IBM Plex Mono", "DejaVu Sans Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
:root[data-theme="mocha"] {
|
||||
color-scheme: dark;
|
||||
--bg: #1e1e2e;
|
||||
--panel: #181825;
|
||||
@@ -16,7 +38,6 @@
|
||||
--danger: #f38ba8;
|
||||
--scrim: #11111bcc;
|
||||
--shadow: #11111b99;
|
||||
--font: "SFMono-Regular", "Cascadia Code", "IBM Plex Mono", "DejaVu Sans Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
:root[data-theme="latte"] {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const THEMES = ["nord", "mocha", "latte"] as const;
|
||||
export type Theme = (typeof THEMES)[number];
|
||||
|
||||
/** The page background of each theme, also used for the browser bar. index.html has a copy. */
|
||||
export const THEME_COLORS: Record<Theme, string> = { nord: "#2e3440", mocha: "#1e1e2e", latte: "#e6e9ef" };
|
||||
|
||||
export function isTheme(value: unknown): value is Theme {
|
||||
return THEMES.includes(value as Theme);
|
||||
}
|
||||
Reference in new issue
Block a user