From 169ff8b8d9fcdf14caa97016c84d5c2d518fde83 Mon Sep 17 00:00:00 2001 From: Josh Lewis Date: Tue, 27 Jan 2026 17:34:05 -0600 Subject: [PATCH] feat(webchat): Add chat font size setting Adds a configurable font size option for the webchat interface. Changes: - Add chatFontSize setting to UiSettings (small/medium/large/x-large) - Add --font-size-chat CSS variable in base.css - Update chat text styles to use the CSS variable - Add applyChatFontSize function to apply setting to document - Add font size dropdown control in chat controls UI - Add styles for the font size control Resolves #3027 --- ui/src/styles/base.css | 1 + ui/src/styles/chat/layout.css | 47 +++++++++++++++++++++++++++++++++ ui/src/styles/chat/text.css | 2 +- ui/src/ui/app-render.helpers.ts | 39 +++++++++++++++++++++++++++ ui/src/ui/app-settings.test.ts | 1 + ui/src/ui/app-settings.ts | 10 ++++++- ui/src/ui/storage.ts | 18 +++++++++++++ 7 files changed, 116 insertions(+), 2 deletions(-) diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index f77cff9ed..2c87cbb92 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -84,6 +84,7 @@ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; --font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-size-chat: 14px; /* Adjustable via UI settings */ /* Shadows - Richer with subtle color */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 589b0b62d..d127e1983 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -325,6 +325,53 @@ color: rgba(16, 24, 40, 0.3); } +/* Font size control */ +.chat-controls__font-size { + display: flex; + align-items: center; + gap: 6px; +} + +.chat-controls__font-icon { + display: flex; + align-items: center; + justify-content: center; + color: var(--muted); +} + +.chat-controls__font-icon svg { + width: 16px; + height: 16px; + stroke: currentColor; + fill: none; + stroke-width: 2px; +} + +.chat-controls__font-select { + padding: 4px 8px; + font-size: 12px; + border-radius: 6px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.06); + color: var(--text); + cursor: pointer; + min-width: 80px; +} + +.chat-controls__font-select:hover { + border-color: var(--border-strong); +} + +.chat-controls__font-select:focus { + outline: none; + border-color: var(--accent); +} + +:root[data-theme="light"] .chat-controls__font-select { + background: #ffffff; + border-color: var(--border); +} + .btn--icon:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.2); diff --git a/ui/src/styles/chat/text.css b/ui/src/styles/chat/text.css index 13e245de2..ddeb089a3 100644 --- a/ui/src/styles/chat/text.css +++ b/ui/src/styles/chat/text.css @@ -19,7 +19,7 @@ } .chat-text { - font-size: 14px; + font-size: var(--font-size-chat, 14px); line-height: 1.5; word-wrap: break-word; overflow-wrap: break-word; diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 22f8d90db..fda74c50a 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -9,6 +9,7 @@ import { syncUrlWithSessionKey } from "./app-settings"; import type { SessionsListResult } from "./types"; import type { ThemeMode } from "./theme"; import type { ThemeTransitionContext } from "./theme-transition"; +import type { ChatFontSize } from "./storage"; export function renderTab(state: AppViewState, tab: Tab) { const href = pathForTab(tab, state.basePath); @@ -128,6 +129,44 @@ export function renderChatControls(state: AppViewState) { > ${focusIcon} + | + ${renderFontSizeControl(state)} + + `; +} + +const FONT_SIZE_OPTIONS: Array<{ value: ChatFontSize; label: string }> = [ + { value: "small", label: "Small" }, + { value: "medium", label: "Medium" }, + { value: "large", label: "Large" }, + { value: "x-large", label: "X-Large" }, +]; + +function renderFontSizeControl(state: AppViewState) { + const currentSize = state.settings.chatFontSize ?? "medium"; + const textIcon = html``; + return html` +
+ ${textIcon} +
`; } diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index aae48df6f..e4d2ac507 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -17,6 +17,7 @@ const createHost = (tab: Tab): SettingsHost => ({ theme: "system", chatFocusMode: false, chatShowThinking: true, + chatFontSize: "medium", splitRatio: 0.6, navCollapsed: false, navGroupsCollapsed: {}, diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index e269742b2..ee25cbfb3 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -10,7 +10,7 @@ import { loadPresence } from "./controllers/presence"; import { loadSessions } from "./controllers/sessions"; import { loadSkills } from "./controllers/skills"; import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation"; -import { saveSettings, type UiSettings } from "./storage"; +import { saveSettings, CHAT_FONT_SIZE_PX, type UiSettings, type ChatFontSize } from "./storage"; import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition"; import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; @@ -46,6 +46,7 @@ export function applySettings(host: SettingsHost, next: UiSettings) { host.theme = next.theme; applyResolvedTheme(host, resolveTheme(next.theme)); } + applyChatFontSize(normalized.chatFontSize); host.applySessionKey = host.settings.lastActiveSessionKey; } @@ -191,6 +192,7 @@ export function inferBasePath() { export function syncThemeWithSettings(host: SettingsHost) { host.theme = host.settings.theme ?? "system"; applyResolvedTheme(host, resolveTheme(host.theme)); + applyChatFontSize(host.settings.chatFontSize ?? "medium"); } export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) { @@ -201,6 +203,12 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) root.style.colorScheme = resolved; } +export function applyChatFontSize(size: ChatFontSize) { + if (typeof document === "undefined") return; + const px = CHAT_FONT_SIZE_PX[size] ?? CHAT_FONT_SIZE_PX.medium; + document.documentElement.style.setProperty("--font-size-chat", `${px}px`); +} + export function attachThemeListener(host: SettingsHost) { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return; host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 4b1836bfb..4b7fc9ab9 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -2,6 +2,15 @@ const KEY = "moltbot.control.settings.v1"; import type { ThemeMode } from "./theme"; +export type ChatFontSize = "small" | "medium" | "large" | "x-large"; + +export const CHAT_FONT_SIZE_PX: Record = { + small: 13, + medium: 14, + large: 16, + "x-large": 18, +}; + export type UiSettings = { gatewayUrl: string; token: string; @@ -10,6 +19,7 @@ export type UiSettings = { theme: ThemeMode; chatFocusMode: boolean; chatShowThinking: boolean; + chatFontSize: ChatFontSize; // Chat text font size splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6) navCollapsed: boolean; // Collapsible sidebar state navGroupsCollapsed: Record; // Which nav groups are collapsed @@ -29,6 +39,7 @@ export function loadSettings(): UiSettings { theme: "system", chatFocusMode: false, chatShowThinking: true, + chatFontSize: "medium", splitRatio: 0.6, navCollapsed: false, navGroupsCollapsed: {}, @@ -69,6 +80,13 @@ export function loadSettings(): UiSettings { typeof parsed.chatShowThinking === "boolean" ? parsed.chatShowThinking : defaults.chatShowThinking, + chatFontSize: + parsed.chatFontSize === "small" || + parsed.chatFontSize === "medium" || + parsed.chatFontSize === "large" || + parsed.chatFontSize === "x-large" + ? parsed.chatFontSize + : defaults.chatFontSize, splitRatio: typeof parsed.splitRatio === "number" && parsed.splitRatio >= 0.4 &&