From 4a3e6dea63ca7d4c739d309b141a699811288caa Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 31 Aug 2026 11:58:42 +0200 Subject: [PATCH] Increase mobile content text to 16px (#4111) Keep mobile interface chrome at 15px while defaulting readable content to 16px. Migrate existing mobile 15px content preferences once while preserving later explicit choices. --- docs/design.md | 2 +- .../src/hooks/use-settings/migrations.test.ts | 46 +++++++++++++++++++ .../app/src/hooks/use-settings/migrations.ts | 25 ++++++++-- .../src/hooks/use-settings/storage.test.ts | 4 +- .../app/src/hooks/use-settings/storage.ts | 6 +-- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/docs/design.md b/docs/design.md index 21557dbedf..bd13192d34 100644 --- a/docs/design.md +++ b/docs/design.md @@ -30,7 +30,7 @@ Before adding a new component, read `components/ui/`. The primitive usually exis Hierarchy is conveyed through weight and color, not size. Most interface text is `fontSize.base`; compact metadata and hints use `fontSize.sm`. The distinction between a row's primary line and its secondary line is `foreground` versus `foregroundMuted`. -The authored interface ramp uses a 14px base. New native installs default to 15px; web and desktop default to 14px. The Appearance **Interface size** setting is the rendered `fontSize.base` value and scales the rest of the UI ramp proportionally. Primary readable content has its own `fontSize.content`, which defaults to 15px on every platform. It owns message bodies, composer input, Markdown, and PR prose. Controls, navigation, metadata, tool chrome, code, diffs, editors, and terminals stay on their interface or code tokens. **Code size** remains independent. +The authored interface ramp uses a 14px base. New native installs default to 15px; web and desktop default to 14px. The Appearance **Interface size** setting is the rendered `fontSize.base` value and scales the rest of the UI ramp proportionally. Primary readable content has its own `fontSize.content`, which defaults to 16px on native and 15px on web and desktop. It owns message bodies, composer input, Markdown, and PR prose. Controls, navigation, metadata, tool chrome, code, diffs, editors, and terminals stay on their interface or code tokens. **Code size** remains independent. Weight has three tiers, applied by role: diff --git a/packages/app/src/hooks/use-settings/migrations.test.ts b/packages/app/src/hooks/use-settings/migrations.test.ts index 5c4e5b1f87..b03cd8138c 100644 --- a/packages/app/src/hooks/use-settings/migrations.test.ts +++ b/packages/app/src/hooks/use-settings/migrations.test.ts @@ -20,6 +20,11 @@ function storedSendBehavior(storage: Storage): SendBehavior | undefined { return raw === undefined ? undefined : JSON.parse(raw).sendBehavior; } +function storedContentFontSize(storage: Storage): number | undefined { + const raw = storage.entries.get(APP_SETTINGS_KEY); + return raw === undefined ? undefined : JSON.parse(raw).contentFontSize; +} + /** An in-memory storage whose write to `failingKey` always throws, as a full disk would. */ function createFailingWriteStorage(failingKey: string): Storage { const storage = createInMemoryKeyValueStorage(); @@ -81,6 +86,47 @@ describe("migrateAppSettings", () => { expect(appliedIds(storage)).toEqual(["some-later-migration", "steer-default"]); }); + it("migrates every mobile 15px content preference to 16px", async () => { + const storage = createInMemoryKeyValueStorage(); + const settings = { ...settingsWith("steer"), contentFontSize: 15 }; + + const result = await migrateAppSettings(settings, storage, undefined, { native: true }); + + expect(result.contentFontSize).toBe(16); + expect(storedContentFontSize(storage)).toBe(16); + expect(appliedIds(storage)).toEqual(["steer-default", "mobile-content-16"]); + }); + + it("leaves a 15px web content preference unchanged", async () => { + const storage = createInMemoryKeyValueStorage(); + const settings = { ...settingsWith("steer"), contentFontSize: 15 }; + + const result = await migrateAppSettings(settings, storage, undefined, { native: false }); + + expect(result.contentFontSize).toBe(15); + expect(storedContentFontSize(storage)).toBeUndefined(); + expect(appliedIds(storage)).toEqual(["steer-default"]); + }); + + it("lets a mobile user choose 15px after the default migration ran", async () => { + const storage = createInMemoryKeyValueStorage(); + await migrateAppSettings( + { ...settingsWith("steer"), contentFontSize: 15 }, + storage, + undefined, + { native: true }, + ); + + const result = await migrateAppSettings( + { ...settingsWith("steer"), contentFontSize: 15 }, + storage, + undefined, + { native: true }, + ); + + expect(result.contentFontSize).toBe(15); + }); + it("stays unmarked when the settings write fails, so a later launch retries", async () => { const storage = createFailingWriteStorage(APP_SETTINGS_KEY); diff --git a/packages/app/src/hooks/use-settings/migrations.ts b/packages/app/src/hooks/use-settings/migrations.ts index 62dd39eaee..3b46a491e5 100644 --- a/packages/app/src/hooks/use-settings/migrations.ts +++ b/packages/app/src/hooks/use-settings/migrations.ts @@ -13,6 +13,9 @@ const AppliedMigrationsSchema = z.strictObject({ applied: z.array(z.string()) }) */ const STEER_DEFAULT_MIGRATION = "steer-default"; +/** Existing mobile installs materialized the old 15px content default in storage. */ +const MOBILE_CONTENT_16_MIGRATION = "mobile-content-16"; + /** * Brings stored settings up to date, returning what the caller should use. Owns both writes so * the marker can only ever be written after the settings it describes: a failed marker write @@ -23,6 +26,7 @@ export async function migrateAppSettings( settings: AppSettings, storage: KeyValueStorage, stored?: PersistedAppSettings, + options: { native?: boolean } = {}, ): Promise { const migrationMarker = await readValidatedJson( storage, @@ -30,12 +34,26 @@ export async function migrateAppSettings( AppliedMigrationsSchema, ); const applied = new Set(migrationMarker?.applied ?? []); - if (applied.has(STEER_DEFAULT_MIGRATION)) { + let addedMigration = false; + + let migrated = settings; + if (!applied.has(STEER_DEFAULT_MIGRATION)) { + migrated = + migrated.sendBehavior === "interrupt" ? { ...migrated, sendBehavior: "steer" } : migrated; + applied.add(STEER_DEFAULT_MIGRATION); + addedMigration = true; + } + + if (options.native && !applied.has(MOBILE_CONTENT_16_MIGRATION)) { + migrated = migrated.contentFontSize === 15 ? { ...migrated, contentFontSize: 16 } : migrated; + applied.add(MOBILE_CONTENT_16_MIGRATION); + addedMigration = true; + } + + if (!addedMigration) { return settings; } - const migrated: AppSettings = - settings.sendBehavior === "interrupt" ? { ...settings, sendBehavior: "steer" } : settings; if (migrated !== settings) { const storedSidebarRowItems = stored?.sidebarRowItems ?? {}; await storage.setItem( @@ -48,7 +66,6 @@ export async function migrateAppSettings( ); } - applied.add(STEER_DEFAULT_MIGRATION); await storage.setItem(SETTINGS_MIGRATIONS_KEY, JSON.stringify({ applied: [...applied] })); return migrated; } diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index be9bef317a..b7c16a2fa9 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -659,8 +659,8 @@ describe("appearance settings", () => { expect(defaultUiBaseFontSize(false)).toBe(14); }); - it("uses a 15px content default on mobile and web", () => { - expect(defaultContentFontSize(true)).toBe(15); + it("uses a 16px content default on mobile and a 15px default on web", () => { + expect(defaultContentFontSize(true)).toBe(16); expect(defaultContentFontSize(false)).toBe(15); expect(DEFAULT_CONTENT_FONT_SIZE).toBe(defaultContentFontSize(false)); }); diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index 4bdf13944c..087be61045 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -52,7 +52,7 @@ export const DEFAULT_UI_BASE_FONT_SIZE = defaultUiBaseFontSize(isNative); export const MIN_UI_BASE_FONT_SIZE = 10; export const MAX_UI_BASE_FONT_SIZE = 21; export function defaultContentFontSize(native: boolean): number { - return native ? 15 : FONT_SIZE.content; + return native ? 16 : FONT_SIZE.content; } export const DEFAULT_CONTENT_FONT_SIZE = defaultContentFontSize(isNative); @@ -75,7 +75,7 @@ export interface AppSettings { uiFontFamily: string; // "" = platform default UI stack monoFontFamily: string; // "" = platform default mono stack uiBaseFontSize: number; // clamped px, platform default 14 or 15 - contentFontSize: number; // clamped px, default 15 + contentFontSize: number; // clamped px, platform default 15 or 16 codeFontSize: number; // clamped px, default 12 syntaxTheme: SyntaxThemeId; // default "one" workspaceTitleSource: WorkspaceTitleSource; @@ -343,7 +343,7 @@ export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise