From f3730e5c1c063c1589ad0c45eec635293957ba12 Mon Sep 17 00:00:00 2001 From: Dipak Date: Sun, 19 Jul 2026 10:22:38 +0530 Subject: [PATCH 1/2] feat(web): add a light theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-way theme choice in Settings (Dark / True black / Light), replacing the OLED checkbox: - theme.ts: ThemeName gains "light"; stored-value validation keeps unknown values falling back to dark; applyTheme sets data-theme for any non-default theme. - tokens.css: a [data-theme=light] block — paper surfaces, ink text, darkened hairlines; the coral accent shifts to its deep value for contrast on paper; warn/err darkened for AA. The code card stays a dark panel deliberately. - TerminalView: light mode swaps the FULL xterm palette (One Light-derived), not just the background — the dark ANSI ramp would be unreadable on paper. Live switch keeps working via rc-theme-change. - theme.test.ts extended for the new value, fallback, and meta mirror. Closes #67 --- packages/web/src/chat/TerminalView.tsx | 69 ++++++++++++++------- packages/web/src/pwa/theme.test.ts | 8 ++- packages/web/src/pwa/theme.ts | 20 +++--- packages/web/src/settings/SettingsPanel.tsx | 29 ++++----- packages/web/src/styles/tokens.css | 63 +++++++++++++++++++ 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/packages/web/src/chat/TerminalView.tsx b/packages/web/src/chat/TerminalView.tsx index b22174a9..00fb6dc6 100644 --- a/packages/web/src/chat/TerminalView.tsx +++ b/packages/web/src/chat/TerminalView.tsx @@ -373,30 +373,57 @@ const THEME = { brightWhite: "#ffffff", } as const; +/** One Light-derived palette. Ghostty paints its own ANSI colors, so on the paper background the dark ramp + * (pale yellow/white text) would be unreadable — swap the whole palette for light, not just the background. */ +const LIGHT_THEME = { + foreground: "#383a42", + cursor: "#383a42", + selectionBackground: "#d2d2d9", + selectionForeground: "#383a42", + black: "#383a42", + red: "#ca1243", + green: "#3f8b3f", + yellow: "#a06500", + blue: "#2f5fd0", + magenta: "#a626a4", + cyan: "#0b7a99", + white: "#fafafa", + brightBlack: "#696c77", + brightRed: "#ca1243", + brightGreen: "#3f8b3f", + brightYellow: "#a06500", + brightBlue: "#2f5fd0", + brightMagenta: "#a626a4", + brightCyan: "#0b7a99", + brightWhite: "#ffffff", +} as const; + function ghosttyTheme(): GhosttyTerminalTheme { + // light swaps the whole palette; dark/oled share the dark ramp and only re-base the background. + const src = loadTheme() === "light" ? LIGHT_THEME : THEME; return { background: TERMINAL_BG[loadTheme()], - foreground: THEME.foreground, - cursor: THEME.cursor, - selectionBackground: THEME.selectionBackground, - selectionForeground: THEME.selectionForeground, + foreground: src.foreground, + cursor: src.cursor, + selectionBackground: src.selectionBackground, + selectionForeground: src.selectionForeground, palette: [ - THEME.black, - THEME.red, - THEME.green, - THEME.yellow, - THEME.blue, - THEME.magenta, - THEME.cyan, - THEME.white, - THEME.brightBlack, - THEME.brightRed, - THEME.brightGreen, - THEME.brightYellow, - THEME.brightBlue, - THEME.brightMagenta, - THEME.brightCyan, - THEME.brightWhite, + src.black, + src.red, + src.green, + src.yellow, + src.blue, + src.magenta, + src.cyan, + src.white, + src.brightBlack, + src.brightRed, + src.brightGreen, + src.brightYellow, + src.brightBlue, + src.brightMagenta, + src.brightCyan, + src.brightWhite, ], }; } @@ -901,7 +928,7 @@ export function GhosttyProductTerminalView({ }, }); termRef.current = term; - // Live theme switch (Settings → OLED toggle) restyles the OPEN terminal without a remount. + // Live theme switch (Settings → theme picker) restyles the OPEN terminal without a remount. const onThemeChange = (): void => { term.options.theme = ghosttyTheme(); }; diff --git a/packages/web/src/pwa/theme.test.ts b/packages/web/src/pwa/theme.test.ts index 5ae085b6..dfd31289 100644 --- a/packages/web/src/pwa/theme.test.ts +++ b/packages/web/src/pwa/theme.test.ts @@ -16,6 +16,9 @@ test("setTheme persists + applies data-theme; switching back removes it", () => setTheme("oled"); expect(loadTheme()).toBe("oled"); expect(document.documentElement.dataset.theme).toBe("oled"); + setTheme("light"); + expect(loadTheme()).toBe("light"); + expect(document.documentElement.dataset.theme).toBe("light"); setTheme("dark"); expect(loadTheme()).toBe("dark"); // dark is the :root default — the attribute must be REMOVED (not set to "dark") so the override block never matches. @@ -29,14 +32,17 @@ test("applyTheme mirrors the theme-color meta when present", () => { document.head.appendChild(meta); applyTheme("oled"); expect(meta.getAttribute("content")).toBe("#000000"); + applyTheme("light"); + expect(meta.getAttribute("content")).toBe("#f7f6f3"); applyTheme("dark"); expect(meta.getAttribute("content")).toBe("#0a0a0b"); meta.remove(); }); -test("the terminal background map covers both themes (the canvas can't inherit CSS vars)", () => { +test("the terminal background map covers every theme (the canvas can't inherit CSS vars)", () => { expect(TERMINAL_BG.oled).toBe("#000000"); expect(TERMINAL_BG.dark).toBe("#0a0a0b"); + expect(TERMINAL_BG.light).toBe("#f7f6f3"); }); test("setTheme announces rc-theme-change so an open terminal can restyle live", () => { diff --git a/packages/web/src/pwa/theme.ts b/packages/web/src/pwa/theme.ts index 628217f8..d58348c2 100644 --- a/packages/web/src/pwa/theme.ts +++ b/packages/web/src/pwa/theme.ts @@ -1,11 +1,12 @@ /** - * Theme preference: the default near-black "dark" vs "oled" (TRUE #000 black — on an OLED panel those pixels - * are simply off, so the app burns less battery and blacks read bottomless). A CLIENT-side preference (same - * localStorage convention as session names): applied by setting `data-theme` on , which tokens.css uses - * to override the surface palette. Applied at boot (main.tsx, before first paint) and instantly from Settings. + * Theme preference: the default near-black "dark", "oled" (TRUE #000 black — on an OLED panel those pixels + * are simply off, so the app burns less battery and blacks read bottomless), or "light" (paper surfaces for + * bright-daylight use). A CLIENT-side preference (same localStorage convention as session names): applied by + * setting `data-theme` on , which tokens.css uses to override the surface palette. Applied at boot + * (main.tsx, before first paint) and instantly from Settings. */ -export type ThemeName = "dark" | "oled"; +export type ThemeName = "dark" | "oled" | "light"; const KEY = "roamcode.theme"; @@ -14,17 +15,20 @@ const KEY = "roamcode.theme"; export const TERMINAL_BG: Record = { dark: "#0a0a0b", oled: "#000000", + light: "#f7f6f3", }; /** The browser-chrome color (status bar / title bar) per theme — mirrored into . */ const THEME_COLOR: Record = { dark: "#0a0a0b", oled: "#000000", + light: "#f7f6f3", }; export function loadTheme(): ThemeName { try { - return localStorage.getItem(KEY) === "oled" ? "oled" : "dark"; + const stored = localStorage.getItem(KEY); + return stored === "oled" || stored === "light" ? stored : "dark"; } catch { return "dark"; } @@ -34,8 +38,8 @@ export function loadTheme(): ThemeName { * Safe anywhere (no-ops without a document). */ export function applyTheme(theme: ThemeName): void { if (typeof document === "undefined") return; - if (theme === "oled") document.documentElement.dataset.theme = "oled"; - else delete document.documentElement.dataset.theme; + if (theme === "dark") delete document.documentElement.dataset.theme; + else document.documentElement.dataset.theme = theme; const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute("content", THEME_COLOR[theme]); } diff --git a/packages/web/src/settings/SettingsPanel.tsx b/packages/web/src/settings/SettingsPanel.tsx index 76f48b2d..c8fabb8f 100644 --- a/packages/web/src/settings/SettingsPanel.tsx +++ b/packages/web/src/settings/SettingsPanel.tsx @@ -361,24 +361,25 @@ export function SettingsPanel({ Theme and session list preferences - {/* OLED true-black: applies INSTANTLY (no save button) — a client-side preference persisted in - this browser's localStorage, like session names. On an OLED panel #000 pixels are off. */} -