diff --git a/app/globals.css b/app/globals.css index 61c4bca47..d2a335eea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -423,7 +423,7 @@ button.extension-widget-trigger:focus-visible { padding: 3px 12px 8px; color: var(--text-muted); font-family: var(--font-mono); - font-size: 14px; + font-size: var(--extension-widget-font-size, 14px); line-height: 1.45; overflow-wrap: anywhere; white-space: pre-wrap; diff --git a/components/ChatAppearance.test.mjs b/components/ChatAppearance.test.mjs index ab56bcc1a..157ee5f8f 100644 --- a/components/ChatAppearance.test.mjs +++ b/components/ChatAppearance.test.mjs @@ -9,7 +9,7 @@ const settingsPanel = await readFile(new URL("./SettingsPanel.tsx", import.meta. const globals = await readFile(new URL("../app/globals.css", import.meta.url), "utf8"); const chatAppearanceHook = await readFile(new URL("../hooks/useChatAppearance.ts", import.meta.url), "utf8"); const jiti = createJiti(import.meta.url); -const { clampChatContentWidth, clampChatContentFontSize } = await jiti.import("../hooks/useChatAppearance.ts"); +const { clampChatContentWidth, clampChatContentFontSize, clampExtensionWidgetFontSize } = await jiti.import("../hooks/useChatAppearance.ts"); const widthVariable = /var\(--chat-content-max-width, 820px\)/g; @@ -49,3 +49,13 @@ test("chat font size preserves the default and bounds stored or supplied values" assert.equal(clampChatContentFontSize(18.7), 19); assert.equal(clampChatContentFontSize(30), 24); }); + +test("extension widget font size preserves the default and bounds stored or supplied values", () => { + for (const value of [undefined, null, "invalid", Infinity, NaN]) { + assert.equal(clampExtensionWidgetFontSize(value), 14); + } + assert.equal(clampExtensionWidgetFontSize(8), 12); + assert.equal(clampExtensionWidgetFontSize("18"), 18); + assert.equal(clampExtensionWidgetFontSize(18.7), 19); + assert.equal(clampExtensionWidgetFontSize(30), 24); +}); diff --git a/components/SettingsPanel.test.mjs b/components/SettingsPanel.test.mjs index b5cf79e79..5b006443e 100644 --- a/components/SettingsPanel.test.mjs +++ b/components/SettingsPanel.test.mjs @@ -70,9 +70,9 @@ test("groups chat display controls together without row backgrounds", () => { assert.doesNotMatch(appearanceSection, /settings-chat-content/); assert.match(chatSection, /className="settings-chat-options"/); - assert.equal((chatSection.match(/className="settings-chat-option(?: |")/g) ?? []).length, 4); + assert.equal((chatSection.match(/className="settings-chat-option(?: |")/g) ?? []).length, 5); assert.equal((chatSection.match(/) { const { locale, setLocale, supportedLocales, t } = useI18n(); const { preference, setThemePreference } = useTheme(); - const { width: chatContentWidth, setWidth: setChatContentWidth, fontSize, setFontSize } = useChatAppearance(); + const { width: chatContentWidth, setWidth: setChatContentWidth, fontSize, setFontSize, extensionWidgetFontSize, setExtensionWidgetFontSize } = useChatAppearance(); const [shellSettings, setShellSettings] = useState(null); const [shellSaving, setShellSaving] = useState(false); const [shellError, setShellError] = useState(null); @@ -220,6 +223,34 @@ function GeneralSettings({ sessionId, onSessionReloaded, quoteSelectionEnabled, onChange={(event) => setFontSize(Number(event.target.value))} /> +
+
+ + {extensionWidgetFontSize}px + setExtensionWidgetFontSize(EXTENSION_WIDGET_FONT_SIZE_DEFAULT)} + > + + +
+ setExtensionWidgetFontSize(Number(event.target.value))} + /> +
{t("settings.quoteSelection")} void>(); @@ -35,6 +41,12 @@ export function clampChatContentFontSize(value: unknown): number { return Math.max(CHAT_CONTENT_FONT_SIZE_MIN, Math.min(CHAT_CONTENT_FONT_SIZE_MAX, Math.round(size))); } +export function clampExtensionWidgetFontSize(value: unknown): number { + const size = Number(value ?? EXTENSION_WIDGET_FONT_SIZE_DEFAULT); + if (!Number.isFinite(size)) return EXTENSION_WIDGET_FONT_SIZE_DEFAULT; + return Math.max(EXTENSION_WIDGET_FONT_SIZE_MIN, Math.min(EXTENSION_WIDGET_FONT_SIZE_MAX, Math.round(size))); +} + function readStoredPreference(key: string): string | null { try { return window.localStorage.getItem(key); @@ -43,10 +55,11 @@ function readStoredPreference(key: string): string | null { } } -function applyAppearance({ width, fontSize }: ChatAppearance): void { +function applyAppearance({ width, fontSize, extensionWidgetFontSize }: ChatAppearance): void { if (typeof document === "undefined") return; document.documentElement.style.setProperty("--chat-content-max-width", `${width}px`); document.documentElement.style.setProperty("--chat-content-font-size", `${fontSize}px`); + document.documentElement.style.setProperty("--extension-widget-font-size", `${extensionWidgetFontSize}px`); } function getSnapshot(): ChatAppearance { @@ -55,6 +68,7 @@ function getSnapshot(): ChatAppearance { appearance = { width: clampChatContentWidth(readStoredPreference(CHAT_CONTENT_WIDTH_STORAGE_KEY)), fontSize: clampChatContentFontSize(readStoredPreference(CHAT_CONTENT_FONT_SIZE_STORAGE_KEY)), + extensionWidgetFontSize: clampExtensionWidgetFontSize(readStoredPreference(EXTENSION_WIDGET_FONT_SIZE_STORAGE_KEY)), }; applyAppearance(appearance); } @@ -66,15 +80,28 @@ function subscribe(listener: () => void): () => void { return () => { listeners.delete(listener); }; } +function clampByKey(key: keyof ChatAppearance, value: number): number { + switch (key) { + case "width": return clampChatContentWidth(value); + case "fontSize": return clampChatContentFontSize(value); + case "extensionWidgetFontSize": return clampExtensionWidgetFontSize(value); + } +} + +function storageKeyFor(key: keyof ChatAppearance): string { + switch (key) { + case "width": return CHAT_CONTENT_WIDTH_STORAGE_KEY; + case "fontSize": return CHAT_CONTENT_FONT_SIZE_STORAGE_KEY; + case "extensionWidgetFontSize": return EXTENSION_WIDGET_FONT_SIZE_STORAGE_KEY; + } +} + function setPreference(key: keyof ChatAppearance, value: number): void { - const nextValue = key === "width" ? clampChatContentWidth(value) : clampChatContentFontSize(value); + const nextValue = clampByKey(key, value); appearance = { ...getSnapshot(), [key]: nextValue }; applyAppearance(appearance); try { - window.localStorage.setItem( - key === "width" ? CHAT_CONTENT_WIDTH_STORAGE_KEY : CHAT_CONTENT_FONT_SIZE_STORAGE_KEY, - String(nextValue), - ); + window.localStorage.setItem(storageKeyFor(key), String(nextValue)); } catch { // Best-effort browser preference persistence. } @@ -83,8 +110,9 @@ function setPreference(key: keyof ChatAppearance, value: number): void { const setWidth = (value: number) => setPreference("width", value); const setFontSize = (value: number) => setPreference("fontSize", value); +const setExtensionWidgetFontSize = (value: number) => setPreference("extensionWidgetFontSize", value); export function useChatAppearance() { const snapshot = useSyncExternalStore(subscribe, getSnapshot, () => DEFAULT_APPEARANCE); - return { ...snapshot, setWidth, setFontSize }; + return { ...snapshot, setWidth, setFontSize, setExtensionWidgetFontSize }; } diff --git a/lib/i18n/messages/en.ts b/lib/i18n/messages/en.ts index 632b630ce..232b61ace 100644 --- a/lib/i18n/messages/en.ts +++ b/lib/i18n/messages/en.ts @@ -21,6 +21,8 @@ export const enLocale: LocalePlugin = { "settings.chatContentFontSize": "Chat font size", "settings.resetChatContentWidth": "Reset chat content width", "settings.resetChatContentFontSize": "Reset chat font size", + "settings.extensionWidgetFontSize": "Extension widget font size", + "settings.resetExtensionWidgetFontSize": "Reset extension widget font size", "settings.chatContentWidthDescription": "Set the maximum width of messages and the composer.", "settings.themeLight": "Light", "settings.themeDark": "Dark", diff --git a/lib/i18n/messages/zh-CN.ts b/lib/i18n/messages/zh-CN.ts index a2f69aa51..550a43f28 100644 --- a/lib/i18n/messages/zh-CN.ts +++ b/lib/i18n/messages/zh-CN.ts @@ -21,6 +21,8 @@ export const zhCNLocale: LocalePlugin = { "settings.chatContentFontSize": "聊天字体大小", "settings.resetChatContentWidth": "重置聊天内容宽度", "settings.resetChatContentFontSize": "重置聊天字体大小", + "settings.extensionWidgetFontSize": "扩展组件字体大小", + "settings.resetExtensionWidgetFontSize": "重置扩展组件字体大小", "settings.chatContentWidthDescription": "设置消息内容和输入框的最大宽度。", "settings.themeLight": "浅色", "settings.themeDark": "深色", diff --git a/lib/i18n/messages/zh-TW.ts b/lib/i18n/messages/zh-TW.ts index c8ddcd17d..f5d6f128a 100644 --- a/lib/i18n/messages/zh-TW.ts +++ b/lib/i18n/messages/zh-TW.ts @@ -21,6 +21,8 @@ export const zhTWLocale: LocalePlugin = { "settings.chatContentFontSize": "聊天字體大小", "settings.resetChatContentWidth": "重設聊天內容寬度", "settings.resetChatContentFontSize": "重設聊天字體大小", + "settings.extensionWidgetFontSize": "擴充功能小工具字體大小", + "settings.resetExtensionWidgetFontSize": "重設擴充功能小工具字體大小", "settings.chatContentWidthDescription": "設定訊息內容和輸入框的最大寬度。", "settings.themeLight": "淺色", "settings.themeDark": "深色",