From 52370c59980b4c3dce7e1bde22a256d3dfd6b5a7 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 13 May 2026 19:18:05 -0500 Subject: [PATCH] feat(ui): add browser-local Control UI text size setting Adds a bounded browser-local Control UI text size setting in Appearance and Quick Settings, persists it in UiSettings, and applies CSS text-scale variables across chat text, composer input, sidebars, and tool cards while preserving mobile Safari input zoom safety. Fixes #8547. Thanks @BunsDev. --- CHANGELOG.md | 1 + docs/web/control-ui.md | 2 + ui/src/styles/base.css | 7 +++ ui/src/styles/chat/layout.css | 6 +- ui/src/styles/chat/layout.test.ts | 8 +++ ui/src/styles/chat/sidebar.css | 8 +-- ui/src/styles/chat/text.css | 4 +- ui/src/styles/chat/text.test.ts | 15 +++++ ui/src/styles/chat/tool-cards.css | 6 +- ui/src/styles/config.css | 56 +++++++++++++++++++ ui/src/ui/app-render.assistant-avatar.test.ts | 2 + ui/src/ui/app-render.ts | 4 ++ ui/src/ui/app-settings.test.ts | 14 +++++ ui/src/ui/app-settings.ts | 15 ++++- ui/src/ui/app-view-state.ts | 1 + ui/src/ui/app.ts | 8 +++ ui/src/ui/storage.node.test.ts | 22 ++++++++ ui/src/ui/storage.ts | 23 ++++++++ ui/src/ui/views/config-quick.test.ts | 18 ++++++ ui/src/ui/views/config-quick.ts | 31 +++++++++- ui/src/ui/views/config.browser.test.ts | 2 + ui/src/ui/views/config.ts | 37 +++++++++++- 22 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 ui/src/styles/chat/text.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c238d34eef1..90d5570c6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Control UI: add a browser-local Text size setting in Appearance and Quick Settings, scaling chat and dense UI text while keeping inputs above the mobile Safari focus-zoom threshold. Fixes #8547. Thanks @BunsDev. - Docs: add a dedicated ds4 provider page with local DeepSeek V4 Flash config, on-demand startup, context sizing, and live verification steps. - Release validation: add a package-installed Docker user-journey lane that verifies onboarding, mocked model setup, external plugin install/uninstall, ClickClack outbound/inbound messaging, Gateway restart survival, and doctor. - Release validation: add package-installed Docker lanes for real TTY onboarding, media and memory persistence, published-package upgrade journeys, and local marketplace plugin install/update/uninstall coverage. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index a08137419c6..b7ecbb1eb09 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -89,6 +89,8 @@ Docs translations are generated for the same non-English locale set, but the doc The Appearance panel keeps the built-in Claw, Knot, and Dash themes, plus one browser-local tweakcn import slot. To import a theme, open [tweakcn editor](https://tweakcn.com/editor/theme), choose or create a theme, click **Share**, and paste the copied theme link into Appearance. The importer also accepts `https://tweakcn.com/r/themes/` registry URLs, editor URLs like `https://tweakcn.com/editor/theme?theme=amethyst-haze`, relative `/themes/` paths, raw theme IDs, and default theme names such as `amethyst-haze`. +Appearance also includes a browser-local Text size setting. The setting is stored with the rest of Control UI preferences, applies to chat text, composer text, tool cards, and chat sidebars, and keeps text inputs at least 16px so mobile Safari does not auto-zoom on focus. + Imported themes are stored only in the current browser profile. They are not written to gateway config and do not sync across devices. Replacing the imported theme updates the one local slot; clearing it switches the active theme back to Claw if the imported theme was selected. ## What it can do (today) diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index 8f36d24be6e..c9e18bd8d31 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -85,6 +85,13 @@ "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-display: var(--font-body); + --control-ui-text-scale: 1; + --control-ui-text-xs: calc(11px * var(--control-ui-text-scale)); + --control-ui-text-sm: calc(12px * var(--control-ui-text-scale)); + --control-ui-text-md: calc(14px * var(--control-ui-text-scale)); + --control-ui-text-lg: calc(16px * var(--control-ui-text-scale)); + --control-ui-input-text-size: max(16px, calc(14px * var(--control-ui-text-scale))); + --chat-text-size: var(--control-ui-text-md); /* Shadows - Subtle, layered depth */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25); diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 5be268b0abe..227ba23b8df 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -517,7 +517,7 @@ resize: none; white-space: pre-wrap; font-family: var(--font-body); - font-size: 14px; + font-size: var(--control-ui-input-text-size); line-height: 1.45; } @@ -597,7 +597,7 @@ border: none; background: transparent; color: var(--text); - font-size: 0.92rem; + font-size: var(--control-ui-input-text-size); font-family: inherit; line-height: 1.4; outline: none; @@ -615,7 +615,7 @@ .agent-chat__stt-interim { padding: 10px 14px 0; color: var(--muted); - font-size: 0.82rem; + font-size: var(--control-ui-text-sm); line-height: 1.35; overflow-wrap: anywhere; } diff --git a/ui/src/styles/chat/layout.test.ts b/ui/src/styles/chat/layout.test.ts index 1c838206750..311a8d4659d 100644 --- a/ui/src/styles/chat/layout.test.ts +++ b/ui/src/styles/chat/layout.test.ts @@ -22,4 +22,12 @@ describe("chat layout styles", () => { expect(css).toContain("font-size: 20px;"); expect(css).toContain("place-items: center;"); }); + + it("keeps composer text scale-driven while preserving mobile input zoom safety", () => { + const css = readLayoutCss(); + + expect(css).toContain("font-size: var(--control-ui-input-text-size);"); + expect(css).toContain(".agent-chat__composer-combobox > textarea"); + expect(css).toContain(".chat-compose .chat-compose__field textarea"); + }); }); diff --git a/ui/src/styles/chat/sidebar.css b/ui/src/styles/chat/sidebar.css index 69e1b8fb2fe..b701fd7d162 100644 --- a/ui/src/styles/chat/sidebar.css +++ b/ui/src/styles/chat/sidebar.css @@ -68,7 +68,7 @@ .sidebar-title { font-weight: 600; - font-size: 14px; + font-size: var(--control-ui-text-md); } .sidebar-content { @@ -78,7 +78,7 @@ } .sidebar-markdown { - font-size: 14px; + font-size: var(--control-ui-text-md); line-height: 1.6; color: var(--text); } @@ -123,7 +123,7 @@ .sidebar-markdown-shell__hint { color: var(--muted); - font-size: 12px; + font-size: var(--control-ui-text-sm); line-height: 1.45; } @@ -158,7 +158,7 @@ } .sidebar-markdown-reader.sidebar-markdown { - font-size: 14.5px; + font-size: calc(14.5px * var(--control-ui-text-scale)); line-height: 1.72; } diff --git a/ui/src/styles/chat/text.css b/ui/src/styles/chat/text.css index bd188eee855..7ced9a865c4 100644 --- a/ui/src/styles/chat/text.css +++ b/ui/src/styles/chat/text.css @@ -9,7 +9,7 @@ border: 1px dashed rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.04); color: var(--muted); - font-size: 12px; + font-size: var(--control-ui-text-sm); line-height: 1.4; } @@ -19,7 +19,7 @@ } .chat-text { - font-size: 14px; + font-size: var(--chat-text-size); line-height: 1.5; word-wrap: break-word; overflow-wrap: break-word; diff --git a/ui/src/styles/chat/text.test.ts b/ui/src/styles/chat/text.test.ts new file mode 100644 index 00000000000..bf319e955b0 --- /dev/null +++ b/ui/src/styles/chat/text.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { readStyleSheet } from "../../../../test/helpers/ui-style-fixtures.js"; + +function readTextCss(): string { + return readStyleSheet("ui/src/styles/chat/text.css"); +} + +describe("chat text styles", () => { + it("uses browser-local text scale variables for message text", () => { + const css = readTextCss(); + + expect(css).toContain("font-size: var(--chat-text-size);"); + expect(css).toContain("font-size: var(--control-ui-text-sm);"); + }); +}); diff --git a/ui/src/styles/chat/tool-cards.css b/ui/src/styles/chat/tool-cards.css index 221b8c8bf31..4fe6981e22e 100644 --- a/ui/src/styles/chat/tool-cards.css +++ b/ui/src/styles/chat/tool-cards.css @@ -64,7 +64,7 @@ align-items: center; gap: 6px; font-weight: 600; - font-size: 14px; + font-size: var(--control-ui-text-md); line-height: 1.2; min-width: 0; } @@ -157,12 +157,12 @@ } .chat-tool-card__status-text { - font-size: 11px; + font-size: var(--control-ui-text-xs); margin-top: 10px; } .chat-tool-card__detail { - font-size: 12px; + font-size: var(--control-ui-text-sm); color: var(--muted); margin-top: 6px; } diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index 7d63e39dad4..b37a97b6d30 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -801,6 +801,62 @@ color: var(--accent); } +.settings-text-scale { + display: grid; + gap: 8px; +} + +.settings-text-scale__options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); + gap: 8px; +} + +.settings-text-scale__btn { + display: grid; + gap: 4px; + justify-items: start; + min-height: 58px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--card); + color: var(--text); + cursor: pointer; + text-align: left; + transition: + border-color var(--duration-fast) ease, + background var(--duration-fast) ease, + transform var(--duration-fast) ease; +} + +.settings-text-scale__btn:hover { + border-color: var(--border-strong); + background: var(--bg-elevated); + transform: translateY(-1px); +} + +.settings-text-scale__btn.active { + border-color: var(--accent); + background: var(--accent-subtle); +} + +.settings-text-scale__sample { + font-weight: 650; + font-size: 13px; + line-height: 1.2; +} + +.settings-text-scale__label { + color: var(--muted); + font-size: 11px; + line-height: 1.2; +} + +.settings-text-scale__btn.active .settings-text-scale__label { + color: var(--accent); +} + .settings-info-grid { display: grid; gap: 10px; diff --git a/ui/src/ui/app-render.assistant-avatar.test.ts b/ui/src/ui/app-render.assistant-avatar.test.ts index 8211311a95f..a3c40779fd5 100644 --- a/ui/src/ui/app-render.assistant-avatar.test.ts +++ b/ui/src/ui/app-render.assistant-avatar.test.ts @@ -52,6 +52,7 @@ function createState(overrides: Partial = {}): AppViewState { navCollapsed: false, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, chatFocusMode: false, chatShowThinking: false, chatShowToolCalls: true, @@ -200,6 +201,7 @@ function createState(overrides: Partial = {}): AppViewState { importCustomTheme: vi.fn(), clearCustomTheme: vi.fn(), setBorderRadius: vi.fn(), + setTextScale: vi.fn(), applySettings: vi.fn(), applyLocalUserIdentity: vi.fn(), loadOverview: vi.fn(), diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index afec74ce5f5..4882c5fd3c7 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -984,6 +984,8 @@ export function renderApp(state: AppViewState) { onClearCustomTheme: () => state.clearCustomTheme(), borderRadius: state.settings.borderRadius, setBorderRadius: (value) => state.setBorderRadius(value), + textScale: state.settings.textScale ?? 100, + setTextScale: (value) => state.setTextScale(value), gatewayUrl: state.settings.gatewayUrl, assistantName: state.assistantName, configPath: state.configSnapshot?.path ?? null, @@ -1142,6 +1144,7 @@ export function renderApp(state: AppViewState) { hasCustomTheme: Boolean(state.settings.customTheme), customThemeLabel: state.settings.customTheme?.label ?? null, borderRadius: state.settings.borderRadius, + textScale: state.settings.textScale ?? 100, setTheme: (theme, context) => state.setTheme(theme, context), onOpenCustomThemeImport: () => { state.setTab("appearance"); @@ -1154,6 +1157,7 @@ export function renderApp(state: AppViewState) { }, setThemeMode: (mode, context) => state.setThemeMode(mode, context), setBorderRadius: (value) => state.setBorderRadius(value), + setTextScale: (value) => state.setTextScale(value), userAvatar: state.userAvatar ?? null, onUserAvatarChange: (avatar) => state.applyLocalUserIdentity?.({ avatar }), assistantAvatar: configAssistantAvatar, diff --git a/ui/src/ui/app-settings.test.ts b/ui/src/ui/app-settings.test.ts index acb9d91d012..4981efeaae9 100644 --- a/ui/src/ui/app-settings.test.ts +++ b/ui/src/ui/app-settings.test.ts @@ -46,6 +46,7 @@ type SettingsHost = { navWidth: number; navGroupsCollapsed: Record; borderRadius: number; + textScale?: import("./storage.ts").TextScaleStop; customTheme?: import("./custom-theme.ts").ImportedCustomTheme; }; theme: ThemeName & ThemeMode; @@ -144,6 +145,7 @@ const createHost = (tab: Tab): SettingsHost => ({ navWidth: 220, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, }, theme: "claw" as unknown as ThemeName & ThemeMode, themeMode: "system", @@ -292,6 +294,18 @@ describe("setTabFromRoute", () => { expect(host.themeResolved).toBe("openknot-light"); }); + it("applies normalized browser-local text scale", () => { + const host = createHost("chat"); + + applySettings(host, { + ...host.settings, + textScale: 125, + }); + + expect(host.settings.textScale).toBe(125); + expect(document.documentElement.style.getPropertyValue("--control-ui-text-scale")).toBe("1.25"); + }); + it("syncs both theme family and mode from persisted settings", () => { const host = createHost("chat"); host.settings.theme = "dash"; diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 894e7878e59..40a6cd6fd84 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -65,6 +65,7 @@ import { type Tab, } from "./navigation.ts"; import { + normalizeTextScale, saveLocalUserIdentity, saveSettings, type LocalUserIdentity, @@ -152,6 +153,7 @@ type SettingsAppHost = SettingsHost & export function applySettings(host: SettingsHost, next: UiSettings) { const normalized = { ...next, + textScale: normalizeTextScale(next.textScale), lastActiveSessionKey: normalizeOptionalString(next.lastActiveSessionKey) ?? normalizeOptionalString(next.sessionKey) ?? @@ -165,7 +167,8 @@ export function applySettings(host: SettingsHost, next: UiSettings) { host.themeMode = next.themeMode; applyResolvedTheme(host, resolveTheme(next.theme, next.themeMode)); } - applyBorderRadius(next.borderRadius); + applyBorderRadius(normalized.borderRadius); + applyTextScale(normalized.textScale); host.applySessionKey = host.settings.lastActiveSessionKey; } @@ -450,6 +453,7 @@ export function syncThemeWithSettings(host: SettingsHost) { } applyResolvedTheme(host, resolveTheme(host.theme, host.themeMode)); applyBorderRadius(host.settings.borderRadius ?? 50); + applyTextScale(host.settings.textScale); syncSystemThemeListener(host); } @@ -474,6 +478,15 @@ export function applyBorderRadius(value: number) { root.style.setProperty("--radius", `${Math.round(BASE_RADII.default * scale)}px`); } +export function applyTextScale(value: unknown) { + if (typeof document === "undefined") { + return; + } + const root = document.documentElement; + const scale = normalizeTextScale(value) / 100; + root.style.setProperty("--control-ui-text-scale", scale.toFixed(2)); +} + export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) { host.themeResolved = resolved; if (typeof document === "undefined") { diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 7138c46ff6f..d12c64a398d 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -439,6 +439,7 @@ export type AppViewState = { importCustomTheme: () => Promise; clearCustomTheme: () => void; setBorderRadius: (value: number) => void; + setTextScale: (value: number) => void; applySettings: (next: UiSettings) => void; applyLocalUserIdentity?: (next: { name?: string | null; avatar?: string | null }) => void; loadOverview: (opts?: { refresh?: boolean }) => Promise; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 0c9d33d453c..2d0743bca93 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -890,6 +890,14 @@ export class OpenClawApp extends LitElement { this.requestUpdate(); } + setTextScale(value: number) { + applySettingsInternal(this as unknown as Parameters[0], { + ...this.settings, + textScale: value as typeof this.settings.textScale, + }); + this.requestUpdate(); + } + announceSessionSwitch(sessionKey: string, label: string) { const id = ++this.sessionSwitchNoticeSeq; if (this.sessionSwitchNoticeTimer !== null) { diff --git a/ui/src/ui/storage.node.test.ts b/ui/src/ui/storage.node.test.ts index 8a81504f61a..6e073e157f6 100644 --- a/ui/src/ui/storage.node.test.ts +++ b/ui/src/ui/storage.node.test.ts @@ -196,6 +196,7 @@ describe("loadSettings default gateway URL derivation", () => { navWidth: 220, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, sessionsByGateway: { "wss://gateway.example:8443/openclaw": { sessionKey: "agent", @@ -229,6 +230,7 @@ describe("loadSettings default gateway URL derivation", () => { navWidth: 220, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, }); const settings = loadSettings(); @@ -325,6 +327,7 @@ describe("loadSettings default gateway URL derivation", () => { navWidth: 220, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, sessionsByGateway: { [gwUrl]: { sessionKey: "main", @@ -335,6 +338,25 @@ describe("loadSettings default gateway URL derivation", () => { expect(sessionStorage.length).toBe(1); }); + it("normalizes persisted text scale to the nearest supported stop", () => { + setTestLocation({ + protocol: "https:", + host: "gateway.example:8443", + pathname: "/", + }); + + const gwUrl = expectedGatewayUrl(""); + localStorage.setItem( + `openclaw.control.settings.v1:${gwUrl}`, + JSON.stringify({ + gatewayUrl: gwUrl, + textScale: 123, + }), + ); + + expect(loadSettings().textScale).toBe(125); + }); + it("clears the current-tab token when saving an empty token", () => { setTestLocation({ protocol: "https:", diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index b13d3284bc9..43961fd629f 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -37,6 +37,9 @@ import { export const BORDER_RADIUS_STOPS = [0, 25, 50, 75, 100] as const; export type BorderRadiusStop = (typeof BORDER_RADIUS_STOPS)[number]; +export const TEXT_SCALE_STOPS = [90, 100, 110, 125, 140] as const; +export type TextScaleStop = (typeof TEXT_SCALE_STOPS)[number]; + function snapBorderRadius(value: number): BorderRadiusStop { let best: BorderRadiusStop = BORDER_RADIUS_STOPS[0]; let bestDist = Math.abs(value - best); @@ -50,6 +53,22 @@ function snapBorderRadius(value: number): BorderRadiusStop { return best; } +export function normalizeTextScale(value: unknown, fallback: TextScaleStop = 100): TextScaleStop { + if (typeof value !== "number" || !Number.isFinite(value)) { + return fallback; + } + let best: TextScaleStop = TEXT_SCALE_STOPS[0]; + let bestDist = Math.abs(value - best); + for (const stop of TEXT_SCALE_STOPS) { + const dist = Math.abs(value - stop); + if (dist < bestDist) { + best = stop; + bestDist = dist; + } + } + return best; +} + export type UiSettings = { gatewayUrl: string; token: string; @@ -65,6 +84,7 @@ export type UiSettings = { navWidth: number; // Sidebar width when expanded (240–400px) navGroupsCollapsed: Record; // Which nav groups are collapsed borderRadius: number; // Corner roundness (0–100, default 50) + textScale?: TextScaleStop; // Browser-local text scale percentage customTheme?: ImportedCustomTheme; locale?: string; }; @@ -206,6 +226,7 @@ export function loadSettings(): UiSettings { navWidth: 220, navGroupsCollapsed: {}, borderRadius: 50, + textScale: 100, }; try { @@ -267,6 +288,7 @@ export function loadSettings(): UiSettings { parsed.borderRadius <= 100 ? snapBorderRadius(parsed.borderRadius) : defaults.borderRadius, + textScale: normalizeTextScale(parsed.textScale, defaults.textScale), customTheme: customTheme ?? undefined, locale: isSupportedLocale(parsed.locale) ? parsed.locale : undefined, }; @@ -386,6 +408,7 @@ function persistSettings(next: UiSettings) { navWidth: next.navWidth, navGroupsCollapsed: next.navGroupsCollapsed, borderRadius: next.borderRadius, + textScale: normalizeTextScale(next.textScale), ...(next.customTheme ? { customTheme: next.customTheme } : {}), sessionsByGateway, ...(next.locale ? { locale: next.locale } : {}), diff --git a/ui/src/ui/views/config-quick.test.ts b/ui/src/ui/views/config-quick.test.ts index b5e1c6f73d4..7dcbcbc3017 100644 --- a/ui/src/ui/views/config-quick.test.ts +++ b/ui/src/ui/views/config-quick.test.ts @@ -64,10 +64,12 @@ function createProps(overrides: Partial = {}): QuickSettings hasCustomTheme: false, customThemeLabel: null, borderRadius: 50, + textScale: 100, setTheme: vi.fn(), onOpenCustomThemeImport: vi.fn(), setThemeMode: vi.fn(), setBorderRadius: vi.fn(), + setTextScale: vi.fn(), userAvatar: null, onUserAvatarChange: vi.fn(), configObject: {}, @@ -172,6 +174,22 @@ describe("renderQuickSettings", () => { ]); }); + it("lets operators change text size from Appearance quick settings", () => { + const setTextScale = vi.fn(); + const container = document.createElement("div"); + + render(renderQuickSettings(createProps({ textScale: 125, setTextScale })), container); + + const textSizeRow = expectRowByLabel(container, "Text size"); + const active = Array.from(textSizeRow.querySelectorAll("button")).find((button) => + button.classList.contains("qs-segmented__btn--active"), + ); + expect(active?.textContent?.trim()).toBe("XL"); + + expectButtonByText(textSizeRow, "XXL").click(); + expect(setTextScale).toHaveBeenCalledWith(140); + }); + it("keeps the local user name fixed and shows the assistant identity", () => { const container = document.createElement("div"); diff --git a/ui/src/ui/views/config-quick.ts b/ui/src/ui/views/config-quick.ts index dbc7e1ee115..141f7a6f827 100644 --- a/ui/src/ui/views/config-quick.ts +++ b/ui/src/ui/views/config-quick.ts @@ -7,7 +7,7 @@ import { html, nothing, type TemplateResult } from "lit"; import { icons } from "../icons.ts"; -import type { BorderRadiusStop } from "../storage.ts"; +import type { BorderRadiusStop, TextScaleStop } from "../storage.ts"; import { normalizeOptionalString } from "../string-coerce.ts"; import type { ThemeTransitionContext } from "../theme-transition.ts"; import type { ThemeMode, ThemeName } from "../theme.ts"; @@ -82,10 +82,12 @@ export type QuickSettingsProps = { hasCustomTheme: boolean; customThemeLabel?: string | null; borderRadius: number; + textScale: number; setTheme: (theme: ThemeName, context?: ThemeTransitionContext) => void; onOpenCustomThemeImport?: () => void; setThemeMode: (mode: ThemeMode, context?: ThemeTransitionContext) => void; setBorderRadius: (value: number) => void; + setTextScale: (value: number) => void; userAvatar?: string | null; onUserAvatarChange?: (next: string | null) => void; @@ -139,6 +141,14 @@ const BORDER_RADIUS_STOPS: Array<{ value: BorderRadiusStop; label: string }> = [ { value: 100, label: "Full" }, ]; +const TEXT_SCALE_OPTIONS: Array<{ value: TextScaleStop; label: string }> = [ + { value: 90, label: "S" }, + { value: 100, label: "M" }, + { value: 110, label: "L" }, + { value: 125, label: "XL" }, + { value: 140, label: "XXL" }, +]; + const THINKING_LEVELS = ["off", "low", "medium", "high"]; const TOOL_PROFILES = ["minimal", "coding", "messaging", "full"]; const LOCAL_USER_LABEL = "You"; @@ -658,6 +668,25 @@ function renderAppearanceCard(props: QuickSettingsProps) { )} +
+ Text size +
+ ${TEXT_SCALE_OPTIONS.map( + (stop) => html` + + `, + )} +
+
`; diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts index f763522e931..773b5cd1f9e 100644 --- a/ui/src/ui/views/config.browser.test.ts +++ b/ui/src/ui/views/config.browser.test.ts @@ -57,6 +57,8 @@ describe("config view", () => { onOpenCustomThemeImport: vi.fn(), borderRadius: 50, setBorderRadius: vi.fn(), + textScale: 100, + setTextScale: vi.fn(), gatewayUrl: "", assistantName: "OpenClaw", }); diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index bc5f3c6ca2f..1cbb2e93196 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -2,7 +2,12 @@ import JSON5 from "json5"; import { html, nothing, type TemplateResult } from "lit"; import { t } from "../../i18n/index.ts"; import { icons } from "../icons.ts"; -import { BORDER_RADIUS_STOPS, type BorderRadiusStop } from "../storage.ts"; +import { + BORDER_RADIUS_STOPS, + TEXT_SCALE_STOPS, + type BorderRadiusStop, + type TextScaleStop, +} from "../storage.ts"; import type { ThemeTransitionContext } from "../theme-transition.ts"; import type { ThemeMode, ThemeName } from "../theme.ts"; import type { ConfigUiHints } from "../types.ts"; @@ -26,6 +31,14 @@ const BORDER_RADIUS_LABELS: Record = { 100: "Full", }; +const TEXT_SCALE_LABELS: Record = { + 90: "Small", + 100: "Default", + 110: "Large", + 125: "XL", + 140: "XXL", +}; + export type WebPushUiState = { supported: boolean; permission: NotificationPermission | "unsupported"; @@ -85,6 +98,8 @@ export type ConfigProps = { onOpenCustomThemeImport?: () => void; borderRadius: number; setBorderRadius: (value: number) => void; + textScale: number; + setTextScale: (value: number) => void; gatewayUrl: string; assistantName: string; configPath?: string | null; @@ -1062,6 +1077,26 @@ function renderAppearanceSection(props: ConfigProps) { +
+

Text size

+
+
+ ${TEXT_SCALE_STOPS.map( + (stop) => html` + + `, + )} +
+
+
+

Connection