diff --git a/apps/desktop/src/components/settings/FontFamilyRow.tsx b/apps/desktop/src/components/settings/FontFamilyRow.tsx index 824ed8d52..51122d3c9 100644 --- a/apps/desktop/src/components/settings/FontFamilyRow.tsx +++ b/apps/desktop/src/components/settings/FontFamilyRow.tsx @@ -26,8 +26,10 @@ import { IconCheck, IconChevronDown, IconSearch } from "../icons"; * Global UI font picker (Settings → Basics → Appearance). Offers the * system default, bundled open-licensed families, and installed system * families; the selected stack is persisted as `AppSettings.fontFamily` - * and applied to `--font-sans` by App. Selecting System default persists an - * empty stack, which every consumer treats as the built-in token stack. + * and applied to `--font-sans` by App. Selecting the localized system-default + * option persists an empty stack, which every consumer treats as the built-in + * token stack. The closed trigger and search haystack use `settings.fontSystemDefault` + * so the English catalog label in `fonts.ts` never reaches the UI. */ export function FontFamilyRow({ settings, @@ -153,17 +155,24 @@ export function FontFamilyRow({ const selectedValue = settings.fontFamily ?? ""; const selectedOption = options.find((option) => option.value === selectedValue) ?? null; + const defaultLabel = t("settings.fontSystemDefault"); const selectedLabel = - selectedOption?.label ?? readableFontFamily(settings.fontFamily ?? ""); + selectedOption?.group === "default" || selectedValue === "" + ? defaultLabel + : selectedOption?.label ?? readableFontFamily(settings.fontFamily ?? ""); const selectedFamily = selectedOption?.family ?? readableFontFamily(selectedValue); const filtered = useMemo(() => { const needle = query.trim().toLowerCase(); if (!needle) return options; - return options.filter((option) => - option.label.toLowerCase().includes(needle), - ); - }, [options, query]); + return options.filter((option) => { + const localized = + option.group === "default" ? defaultLabel.toLowerCase() : option.label.toLowerCase(); + return ( + localized.includes(needle) || option.label.toLowerCase().includes(needle) + ); + }); + }, [defaultLabel, options, query]); const groupLabel = useCallback((group: string) => { if (group === "bundled") return t("settings.fontBundled"); diff --git a/apps/desktop/src/styles/settings.css b/apps/desktop/src/styles/settings.css index 4d8f196ce..be03522c9 100644 --- a/apps/desktop/src/styles/settings.css +++ b/apps/desktop/src/styles/settings.css @@ -760,21 +760,26 @@ font-size: var(--text-sm-plus); } -/* Global UI font picker (Appearance card). Pill trigger mirrors the - shared settings select; the menu portals to document.body as a fixed - floating layer so the settings card's overflow cannot clip it. */ +/* Global UI font picker (Appearance card). Pill trigger mirrors language / + theme: hug the current label, capped by the settings control column. The + menu portals to document.body as a fixed floating layer so the settings + card's overflow cannot clip it. */ .settings-font { position: relative; + width: max-content; + max-width: 100%; + min-width: 0; } .settings-font-trigger { - display: flex; + display: inline-flex; align-items: center; gap: 8px; - min-width: 200px; - max-width: 280px; + width: max-content; + max-width: 100%; + min-width: 0; height: 30px; - padding: 0 12px; + padding: 0 10px 0 12px; border-radius: var(--radius-full); background: var(--ds-raised); box-shadow: var(--ds-raised-shadow); @@ -788,7 +793,7 @@ .settings-font-trigger-label { min-width: 0; - flex: 1; + flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/apps/desktop/test/settings-font-picker.test.mjs b/apps/desktop/test/settings-font-picker.test.mjs index d86bfcb91..ccd8f0e49 100644 --- a/apps/desktop/test/settings-font-picker.test.mjs +++ b/apps/desktop/test/settings-font-picker.test.mjs @@ -25,6 +25,22 @@ test("selecting System default persists an empty stack so the override clears", assert.doesNotMatch(rowSource, /fontFamily: undefined/); }); +test("the closed trigger and search use the localized system-default label", () => { + assert.match(rowSource, /const defaultLabel = t\("settings\.fontSystemDefault"\)/); + assert.match( + rowSource, + /selectedOption\?\.group === "default" \|\| selectedValue === ""/, + ); + assert.match(rowSource, /option\.group === "default" \? defaultLabel\.toLowerCase\(\)/); +}); + +test("the font trigger hugs the current label like language and theme", () => { + assert.match(styles, /\.settings-font\s*\{[^}]*width:\s*max-content;/s); + assert.match(styles, /\.settings-font-trigger\s*\{[^}]*width:\s*max-content;/s); + assert.match(styles, /\.settings-font-trigger-label\s*\{[^}]*flex:\s*0 1 auto;/s); + assert.doesNotMatch(styles, /\.settings-font-trigger\s*\{[^}]*min-width:\s*200px;/s); +}); + test("font list windows the rows so only the visible slice is in the DOM", () => { assert.match(rowSource, /visibleRowRange\(layout, scrollTop/); assert.match(rowSource, /layout\.rows\.slice\(start, end\)/);