diff --git a/apps/web/src/appearanceFonts.test.ts b/apps/web/src/appearanceFonts.test.ts index 3fbc579e4bc1..0c0db3973655 100644 --- a/apps/web/src/appearanceFonts.test.ts +++ b/apps/web/src/appearanceFonts.test.ts @@ -191,7 +191,7 @@ describe("appearance font variables", () => { smoothing: true, }); - expect(setProperty).toHaveBeenCalledWith("--font-size-code", "14px"); + expect(setProperty).toHaveBeenCalledWith("--diffs-font-size", "14px"); expect(setProperty).toHaveBeenCalledWith("--font-size-tool-output", "10px"); }); }); diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts index f21b8491e36a..555c132b9e72 100644 --- a/apps/web/src/appearanceFonts.ts +++ b/apps/web/src/appearanceFonts.ts @@ -122,7 +122,6 @@ export function applyAppearanceFontVariables( root.style.fontSize = `${clampInterfaceFontSize(preferences.sizeInterface)}px`; root.style.setProperty("--font-size-prompt", `${clampPromptFontSize(preferences.sizePrompt)}px`); const code = clampCodeFontSize(preferences.sizeCode); - root.style.setProperty("--font-size-code", `${code}px`); root.style.setProperty( "--font-size-tool-output", `${clampToolOutputFontSize(preferences.sizeToolOutput)}px`, diff --git a/apps/web/src/components/settings/SettingsPanels.logic.test.ts b/apps/web/src/components/settings/SettingsPanels.logic.test.ts index a757b340923d..f3ba6fdb092f 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.test.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.test.ts @@ -20,7 +20,7 @@ import { } from "./SettingsPanels.logic"; describe("typography settings restore", () => { - it("detects family and size changes by font row", () => { + it("detects family and size changes as separate controls", () => { expect(getChangedTypographySettingLabels(DEFAULT_UNIFIED_SETTINGS)).toEqual([]); expect( getChangedTypographySettingLabels({ @@ -29,7 +29,7 @@ describe("typography settings restore", () => { fontFamilyCode: "Fira Code", fontSizeToolOutput: 12, }), - ).toEqual(["Interface font", "Code font", "Tool output"]); + ).toEqual(["Monospace typeface", "Interface and response size", "Tool output size"]); }); }); diff --git a/apps/web/src/components/settings/SettingsPanels.logic.ts b/apps/web/src/components/settings/SettingsPanels.logic.ts index b3e9df6a7a8c..0086ceec1983 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.ts @@ -87,27 +87,33 @@ type TypographySettings = Pick< | "fontSizeTerminal" >; -/** Labels the font rows whose family or size differs from the defaults. */ +/** Labels each typography control whose value differs from the defaults. */ export function getChangedTypographySettingLabels(settings: TypographySettings): string[] { return [ - ...(settings.fontFamilySans !== DEFAULT_UNIFIED_SETTINGS.fontFamilySans || - settings.fontSizeInterface !== DEFAULT_UNIFIED_SETTINGS.fontSizeInterface - ? ["Interface font"] + ...(settings.fontFamilySans !== DEFAULT_UNIFIED_SETTINGS.fontFamilySans + ? ["Interface typeface"] : []), - ...(settings.fontFamilyComposer !== DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer || - settings.fontSizePrompt !== DEFAULT_UNIFIED_SETTINGS.fontSizePrompt - ? ["Prompt font"] + ...(settings.fontFamilyComposer !== DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer + ? ["Prompt typeface"] : []), - ...(settings.fontFamilyCode !== DEFAULT_UNIFIED_SETTINGS.fontFamilyCode || - settings.fontSizeCode !== DEFAULT_UNIFIED_SETTINGS.fontSizeCode - ? ["Code font"] + ...(settings.fontFamilyCode !== DEFAULT_UNIFIED_SETTINGS.fontFamilyCode + ? ["Monospace typeface"] + : []), + ...(settings.fontFamilyTerminal !== DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal + ? ["Terminal typeface"] + : []), + ...(settings.fontSizeInterface !== DEFAULT_UNIFIED_SETTINGS.fontSizeInterface + ? ["Interface and response size"] + : []), + ...(settings.fontSizePrompt !== DEFAULT_UNIFIED_SETTINGS.fontSizePrompt ? ["Prompt size"] : []), + ...(settings.fontSizeCode !== DEFAULT_UNIFIED_SETTINGS.fontSizeCode + ? ["Diff and file size"] : []), ...(settings.fontSizeToolOutput !== DEFAULT_UNIFIED_SETTINGS.fontSizeToolOutput - ? ["Tool output"] + ? ["Tool output size"] : []), - ...(settings.fontFamilyTerminal !== DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal || - settings.fontSizeTerminal !== DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal - ? ["Terminal font"] + ...(settings.fontSizeTerminal !== DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal + ? ["Terminal size"] : []), ]; } diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 08ab356ba01d..f3fb455eb9bf 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1110,30 +1110,34 @@ function InterfaceFontRow({ preview }: { preview?: ReactNode }) { return ( updateSettings({ fontFamilySans })} - onReset={() => - updateSettings({ - fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans, - fontSizeInterface: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface, - }) - } - size={{ - label: "Interface font size", - min: MIN_INTERFACE_FONT_SIZE, - max: MAX_INTERFACE_FONT_SIZE, - value: settings.fontSizeInterface, - defaultValue: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface, - onChange: (fontSizeInterface) => updateSettings({ fontSizeInterface }), - }} + onReset={() => updateSettings({ fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans })} {...(preview !== undefined ? { preview } : {})} /> ); } +function InterfaceFontSizeRow() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + return ( + updateSettings({ fontSizeInterface })} + /> + ); +} + function PromptFontRow() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); @@ -1141,36 +1145,40 @@ function PromptFontRow() { return ( updateSettings({ fontFamilyComposer })} onReset={() => - updateSettings({ - fontFamilyComposer: DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer, - fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt, - }) + updateSettings({ fontFamilyComposer: DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer }) } - size={{ - label: "Prompt font size", - min: MIN_PROMPT_FONT_SIZE, - max: MAX_PROMPT_FONT_SIZE, - value: settings.fontSizePrompt, - defaultValue: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt, - onChange: (fontSizePrompt) => updateSettings({ fontSizePrompt }), - }} preview={} /> ); } +function PromptFontSizeRow() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + return ( + updateSettings({ fontSizePrompt })} + /> + ); +} + function CodeFontRow({ - title, - description = "Code blocks, diffs, and file previews.", + description = "Response code, diffs, and file previews.", preview, }: { - title?: string; description?: string; preview?: ReactNode; }) { @@ -1180,32 +1188,39 @@ function CodeFontRow({ return ( updateSettings({ fontFamilyCode })} - onReset={() => - updateSettings({ - fontFamilyCode: DEFAULT_UNIFIED_SETTINGS.fontFamilyCode, - fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode, - }) - } + onReset={() => updateSettings({ fontFamilyCode: DEFAULT_UNIFIED_SETTINGS.fontFamilyCode })} requireMonospace - size={{ - label: "Code font size", - min: MIN_CODE_FONT_SIZE, - max: MAX_CODE_FONT_SIZE, - value: settings.fontSizeCode, - defaultValue: DEFAULT_UNIFIED_SETTINGS.fontSizeCode, - onChange: (fontSizeCode) => updateSettings({ fontSizeCode }), - }} preview={preview ?? } /> ); } +function CodeFontSizeRow({ includeTerminal = false }: { includeTerminal?: boolean }) { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + return ( + updateSettings({ fontSizeCode })} + /> + ); +} + function TerminalFontRow() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); @@ -1213,26 +1228,15 @@ function TerminalFontRow() { return ( updateSettings({ fontFamilyTerminal })} onReset={() => - updateSettings({ - fontFamilyTerminal: DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal, - fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal, - }) + updateSettings({ fontFamilyTerminal: DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal }) } requireMonospace - size={{ - label: "Terminal font size", - min: MIN_TERMINAL_FONT_SIZE, - max: MAX_TERMINAL_FONT_SIZE, - value: settings.fontSizeTerminal, - defaultValue: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal, - onChange: (fontSizeTerminal) => updateSettings({ fontSizeTerminal }), - }} preview={ updateSettings({ fontSizeTerminal })} + /> + ); +} + +function ToolOutputFontSizeRow() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); return ( @@ -1268,7 +1289,7 @@ function ToolOutputFontRow() { } control={ - - ); } /** - * The two-font view: one sans, one monospace. The prompt follows the - * interface font and the terminal follows the monospace font, so the demos - * under each row show every surface the choice reaches. + * The two-typeface view: one sans, one monospace. The previews under each row + * show the surfaces reached by the currently visible family controls. */ -function SimpleFontRows() { +function SimpleTypefaceRows() { const settings = usePrimarySettings(); return ( <> } /> @@ -1378,27 +1395,61 @@ function SimpleFontRows() { } /> - ); } +function AdvancedSizeRows() { + return ( + <> + + + + + + + ); +} + +function SimpleSizeRows() { + return ( + <> + + + + + ); +} + +function TypographySubsection({ title, children }: { title: string; children: ReactNode }) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} + // Font smoothing only renders on macOS, so a search jump to it elsewhere // must not flip the section - the target would never mount to be scrolled to. const ADVANCED_TYPOGRAPHY_TARGET_IDS: ReadonlySet = new Set([ "prompt-font", + "prompt-size", "terminal-font", + "terminal-size", ...(typeof navigator !== "undefined" && isMacPlatform(navigator.platform) ? ["font-smoothing"] : []), ]); /** - * The two-font view by default - one sans, one monospace, each cascading to - * every surface it reaches - with an Advanced switch in the section header - * that reveals the per-surface override rows. The choice persists locally, - * and a settings-search jump to an override row flips Advanced on so the - * target exists to scroll to. + * Families and sizes are separate concepts even when they share an underlying + * setting record. The default view shows the two typefaces and the three + * useful size scopes; Advanced adds prompt and terminal overrides to both. + * A settings-search jump to an override row flips Advanced on so the target + * exists to scroll to. */ function TypographySection() { const [advanced, setAdvanced] = useLocalStorage( @@ -1431,8 +1482,16 @@ function TypographySection() { } > - {advanced ? : } - + + {advanced ? : } + + + {advanced ? : } + + + {advanced ? : null} + + ); } @@ -1486,7 +1545,6 @@ function FontFamilySettingsRow({ onValueChange, onReset, requireMonospace = false, - size, }: { id?: string; title: string; @@ -1500,22 +1558,11 @@ function FontFamilySettingsRow({ onValueChange: (value: string) => void; onReset: () => void; requireMonospace?: boolean; - size: { - label: string; - min: number; - max: number; - value: number; - defaultValue: number; - onChange: (v: number) => void; - }; }) { const trimmed = value.trim(); - const resetToDefault = () => { - onReset(); - }; const resetAction = - value !== defaultValue || size.value !== size.defaultValue ? ( - + value !== defaultValue ? ( + ) : null; // The picker always supports exact-name entry. Permission only controls // whether the browser supplies the complete installed-family list. @@ -1528,31 +1575,57 @@ function FontFamilySettingsRow({ onSelect={onValueChange} /> ); - const control = ( -
-
{familyControl}
- -
- ); return ( {familyControl}} > {preview} ); } +function FontSizeSettingsRow({ + id, + title, + description, + label, + min, + max, + value, + defaultValue, + onChange, +}: { + id?: string; + title: string; + description: string; + label: string; + min: number; + max: number; + value: number; + defaultValue: number; + onChange: (value: number) => void; +}) { + return ( + onChange(defaultValue)} /> + ) : null + } + control={ + + } + /> + ); +} + const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ?? 3; function AutoSettleDaysInput({ diff --git a/apps/web/src/components/settings/settingsSearch.test.ts b/apps/web/src/components/settings/settingsSearch.test.ts index 09fd7a9a6a0b..f9b4976291a1 100644 --- a/apps/web/src/components/settings/settingsSearch.test.ts +++ b/apps/web/src/components/settings/settingsSearch.test.ts @@ -90,4 +90,20 @@ describe("searchSettings", () => { targetId: "appearance", }); }); + + it("indexes typography typefaces and sizes as separate settings", () => { + expect(searchSettings("typeface").map((item) => item.id)).toEqual([ + "interface-font", + "prompt-font", + "code-font", + "terminal-font", + ]); + expect(searchSettings("size").map((item) => item.id)).toEqual([ + "interface-size", + "prompt-size", + "code-size", + "tool-output-font", + "terminal-size", + ]); + }); }); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 2c8bbfd08230..af55a4e99eef 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -70,27 +70,47 @@ export const SETTINGS_SEARCH_ITEMS = [ }, { id: "interface-font", - title: "Interface font", + title: "Interface typeface", to: "/settings/appearance", }, { id: "prompt-font", - title: "Prompt font", + title: "Prompt typeface", to: "/settings/appearance", }, { id: "code-font", - title: "Code font", + title: "Monospace typeface", + to: "/settings/appearance", + }, + { + id: "terminal-font", + title: "Terminal typeface", + to: "/settings/appearance", + }, + { + id: "interface-size", + title: "Interface and response size", + to: "/settings/appearance", + }, + { + id: "prompt-size", + title: "Prompt size", + to: "/settings/appearance", + }, + { + id: "code-size", + title: "Diff and file size", to: "/settings/appearance", }, { id: "tool-output-font", - title: "Tool output", + title: "Tool output size", to: "/settings/appearance", }, { - id: "terminal-font", - title: "Terminal font", + id: "terminal-size", + title: "Terminal size", to: "/settings/appearance", }, { diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 83672d43ec49..b7a0c4d8ea5b 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1545,14 +1545,13 @@ code { font-family: var(--font-mono); } -/* Code blocks in chat carry the size preference. Scoped to chat markdown - rather than every pre: a global rule would beat text-size utilities and - inherited sizes on unrelated pre surfaces (terminal previews, approvals). - Inline code stays relative to its sentence so it never towers over prose; - diffs and file previews take the size through --diffs-font-size. */ +/* Code blocks in chat belong to the surrounding response typography, so + fenced and inline code stay proportional to the prose around them. Diffs + and file previews remain editor-like surfaces and take the configurable + code size through --diffs-font-size. */ .chat-markdown .chat-markdown-shiki .shiki, .chat-markdown pre code { - font-size: var(--font-size-code, inherit); + font-size: inherit; } /* @pierre/diffs surfaces (diffs, file previews, annotatable code, search