Give readable content its own text size (#3637)

* feat(app): separate content typography from interface size

Readable prose benefits from its own scale without inflating navigation and controls. Preserve existing users’ rendered size during migration while giving fresh desktop installs a 15px content default.

* test(app): cover independent content typography

The appearance journey now verifies that changing interface size leaves content size intact. Timeline traversal yields when a test deliberately holds the requested page, so the helper no longer waits on its own gate.

* test(desktop): cover independent content typography

The desktop appearance regression follows the renamed Interface control and verifies it does not alter Content size.
This commit is contained in:
Mohamed Boudra
2026-08-21 12:35:23 +02:00
committed by GitHub
parent c032df5b3d
commit 602860625c
31 changed files with 419 additions and 99 deletions
@@ -45,7 +45,7 @@ test("keeps the selected workspace visible in Pure black", async ({ page }, test
}
});
test("applies the base font size to settings text", async ({ page }) => {
test("applies the interface font size to settings text", async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiBaseFontSize: 21 }));
});
@@ -56,11 +56,14 @@ test("applies the base font size to settings text", async ({ page }) => {
const sectionTitle = page.getByText("Theme", { exact: true }).first();
await expect(sectionTitle).toHaveCSS("font-size", "18px");
const fontSizeInput = page.getByLabel("Base font size");
await expect(fontSizeInput).toHaveValue("21");
await fontSizeInput.fill("12");
await fontSizeInput.press("Tab");
const interfaceSizeInput = page.getByLabel("Interface font size");
const contentSizeInput = page.getByLabel("Content font size");
await expect(interfaceSizeInput).toHaveValue("21");
await expect(contentSizeInput).toHaveValue("21");
await interfaceSizeInput.fill("12");
await interfaceSizeInput.press("Tab");
await expect(fontSizeInput).toHaveValue("12");
await expect(interfaceSizeInput).toHaveValue("12");
await expect(contentSizeInput).toHaveValue("21");
await expect(sectionTitle).toHaveCSS("font-size", "10px");
});
@@ -545,15 +545,22 @@ async function returnTimelineToSettledHistoryStart(
page: Page,
moveUp: () => Promise<unknown>,
): Promise<void> {
const loadingSpinner = page.getByTestId("load-older-history-spinner");
// A caller may only start a new history traversal after its prior page has settled.
await expect(loadingSpinner).toBeHidden();
let previous = await readTimelineViewport(page);
while (true) {
await moveUp();
await waitForTimelineGeometryToSettle(page);
let viewport = await readTimelineViewport(page);
// Tests that hold the response need to observe the requested page before releasing it.
if (await loadingSpinner.isVisible()) {
return;
}
if (viewport.scrollTop <= HISTORY_START_THRESHOLD_PX) {
return;
}
await expect(page.getByTestId("load-older-history-spinner")).toBeHidden();
await expect(loadingSpinner).toBeHidden();
await waitForTimelineGeometryToSettle(page);
viewport = await readTimelineViewport(page);
expect(
+13
View File
@@ -27,6 +27,7 @@ interface FakeTheme {
fontFamily: { ui: string; mono: string };
fontSize: {
code: number;
content: number;
sm: number;
base: number;
lg: number;
@@ -45,6 +46,7 @@ function makeFakeTheme(): FakeTheme {
fontFamily: { ui: "seed-ui-stack", mono: "seed-mono-stack" },
fontSize: {
code: 12,
content: 15,
sm: 12,
base: 14,
lg: 16,
@@ -63,6 +65,7 @@ function makeInput(overrides: Partial<AppearanceInput> = {}): AppearanceInput {
uiFontFamily: "",
monoFontFamily: "",
uiBaseFontSize: 14,
contentFontSize: 15,
codeFontSize: 12,
syntaxTheme: "one",
...overrides,
@@ -131,6 +134,7 @@ describe("applyAppearance", () => {
const alreadyScaled = makeFakeTheme();
alreadyScaled.fontSize = {
code: 4,
content: 4,
sm: 4,
base: 4,
lg: 4,
@@ -155,6 +159,15 @@ describe("applyAppearance", () => {
expect(fontSize.code).toBe(10);
});
it("sets content size independently of the interface and code sizes", () => {
applyAppearance(makeInput({ uiBaseFontSize: 14, contentFontSize: 19, codeFontSize: 10 }));
const { fontSize } = runCapturedUpdater();
expect(fontSize.base).toBe(14);
expect(fontSize.content).toBe(19);
expect(fontSize.code).toBe(10);
});
it("sets fontSize.code to codeFontSize regardless of the UI font size", () => {
applyAppearance(makeInput({ uiBaseFontSize: 14, codeFontSize: 18 }));
+12 -2
View File
@@ -15,6 +15,7 @@ export interface AppearanceInput {
uiFontFamily: string; // "" -> default stack
monoFontFamily: string; // "" -> default stack
uiBaseFontSize: number; // already clamped
contentFontSize: number; // already clamped
codeFontSize: number; // already clamped
syntaxTheme: SyntaxThemeId;
}
@@ -28,7 +29,11 @@ export interface AppearanceInput {
* `code` is set absolutely to `codeSize`, never scaled by the UI factor — a separate
* control on a separate semantic axis (mono/diff text).
*/
function scaleFontSize(uiBaseSize: number, codeSize: number): Theme["fontSize"] {
function scaleFontSize(
uiBaseSize: number,
contentSize: number,
codeSize: number,
): Theme["fontSize"] {
const r = uiBaseSize / FONT_SIZE.base;
return {
sm: Math.round(FONT_SIZE.sm * r),
@@ -38,6 +43,7 @@ function scaleFontSize(uiBaseSize: number, codeSize: number): Theme["fontSize"]
"2xl": Math.round(FONT_SIZE["2xl"] * r),
"3xl": Math.round(FONT_SIZE["3xl"] * r),
"4xl": Math.round(FONT_SIZE["4xl"] * r),
content: contentSize, // absolute, NOT scaled
code: codeSize, // absolute, NOT scaled
};
}
@@ -68,7 +74,11 @@ export function applyAppearance(input: AppearanceInput): void {
for (const key of themeKeys) {
UnistylesRuntime.updateTheme(key, (t) => {
const fontFamily = { ui, mono };
const fontSize = scaleFontSize(input.uiBaseFontSize, input.codeFontSize);
const fontSize = scaleFontSize(
input.uiBaseFontSize,
input.contentFontSize,
input.codeFontSize,
);
const lineHeight = { ...t.lineHeight, diff: diffLineHeight };
if (t.colorScheme === "light") {
return {
+2
View File
@@ -57,6 +57,7 @@ export function AppearanceProvider({ children }: { children: ReactNode }) {
uiFontFamily: settings.uiFontFamily,
monoFontFamily: settings.monoFontFamily,
uiBaseFontSize: settings.uiBaseFontSize,
contentFontSize: settings.contentFontSize,
codeFontSize: settings.codeFontSize,
syntaxTheme: settings.syntaxTheme,
});
@@ -67,6 +68,7 @@ export function AppearanceProvider({ children }: { children: ReactNode }) {
settings.uiFontFamily,
settings.monoFontFamily,
settings.uiBaseFontSize,
settings.contentFontSize,
settings.codeFontSize,
settings.syntaxTheme,
]);
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { darkTheme } from "@/styles/theme";
import { appearanceStyleBoundaryKey } from "./appearance-style-boundary";
describe("appearanceStyleBoundaryKey", () => {
it("changes when content size changes without any other appearance token changing", () => {
const contentOnlyChange = {
...darkTheme,
fontSize: { ...darkTheme.fontSize, content: darkTheme.fontSize.content + 1 },
};
expect(appearanceStyleBoundaryKey(contentOnlyChange)).not.toBe(
appearanceStyleBoundaryKey(darkTheme),
);
});
});
@@ -11,8 +11,8 @@ function AppearanceStyleBoundaryBase({ appearanceKey, children }: AppearanceStyl
return <Fragment key={appearanceKey}>{children}</Fragment>;
}
const appearanceStyleBoundaryMapping = (theme: Theme): Partial<AppearanceStyleBoundaryProps> => ({
appearanceKey: [
export function appearanceStyleBoundaryKey(theme: Theme): string {
return [
theme.fontFamily.ui,
theme.fontFamily.mono,
theme.fontSize.sm,
@@ -22,6 +22,7 @@ const appearanceStyleBoundaryMapping = (theme: Theme): Partial<AppearanceStyleBo
theme.fontSize["2xl"],
theme.fontSize["3xl"],
theme.fontSize["4xl"],
theme.fontSize.content,
theme.fontSize.code,
theme.lineHeight.diff,
theme.colors.foreground,
@@ -51,8 +52,12 @@ const appearanceStyleBoundaryMapping = (theme: Theme): Partial<AppearanceStyleBo
theme.colors.syntax.meta,
theme.colors.syntax.heading,
theme.colors.syntax.link,
].join("\u0000"),
});
].join("\u0000");
}
function appearanceStyleBoundaryMapping(theme: Theme): Partial<AppearanceStyleBoundaryProps> {
return { appearanceKey: appearanceStyleBoundaryKey(theme) };
}
const ThemedAppearanceStyleBoundary = withUnistyles(AppearanceStyleBoundaryBase);
+7 -4
View File
@@ -354,9 +354,12 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
},
text: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.content,
...(isWeb
? { lineHeight: Math.round(theme.fontSize.base * 1.4), overflowWrap: "anywhere" as const }
? {
lineHeight: Math.round(theme.fontSize.content * 1.4),
overflowWrap: "anywhere" as const,
}
: {}),
},
imagePreviewContainer: {
@@ -1958,8 +1961,8 @@ const speakMessageStylesheet = StyleSheet.create((theme) => ({
},
text: {
fontFamily: theme.fontFamily.ui,
fontSize: theme.fontSize.base,
lineHeight: Math.round(theme.fontSize.base * 1.4),
fontSize: theme.fontSize.content,
lineHeight: Math.round(theme.fontSize.content * 1.4),
color: theme.colors.foreground,
},
}));
+2 -2
View File
@@ -1980,9 +1980,9 @@ const styles = StyleSheet.create((theme: Theme) => ({
textInput: {
width: "100%",
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.content,
fontWeight: theme.fontWeight.normal,
lineHeight: theme.fontSize.base * 1.4,
lineHeight: theme.fontSize.content * 1.4,
...(isWeb
? ({
outlineStyle: "none",
@@ -16,13 +16,16 @@ import {
DEFAULT_APP_SETTINGS,
DEFAULT_CLIENT_SETTINGS,
DEFAULT_CODE_FONT_SIZE,
DEFAULT_CONTENT_FONT_SIZE,
DEFAULT_TERMINAL_SCROLLBACK_LINES,
DEFAULT_THEME_PREFERENCE,
DEFAULT_UI_BASE_FONT_SIZE,
MAX_CODE_FONT_SIZE,
MAX_CONTENT_FONT_SIZE,
MAX_TERMINAL_SCROLLBACK_LINES,
MAX_UI_BASE_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_CONTENT_FONT_SIZE,
MIN_TERMINAL_SCROLLBACK_LINES,
MIN_UI_BASE_FONT_SIZE,
loadAppSettingsFromStorage as loadAppSettingsFromStoragePure,
@@ -49,13 +52,16 @@ export {
DEFAULT_APP_SETTINGS,
DEFAULT_CLIENT_SETTINGS,
DEFAULT_CODE_FONT_SIZE,
DEFAULT_CONTENT_FONT_SIZE,
DEFAULT_TERMINAL_SCROLLBACK_LINES,
DEFAULT_THEME_PREFERENCE,
DEFAULT_UI_BASE_FONT_SIZE,
MAX_CODE_FONT_SIZE,
MAX_CONTENT_FONT_SIZE,
MAX_TERMINAL_SCROLLBACK_LINES,
MAX_UI_BASE_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_CONTENT_FONT_SIZE,
MIN_TERMINAL_SCROLLBACK_LINES,
MIN_UI_BASE_FONT_SIZE,
parseClampedFontSize,
@@ -6,8 +6,10 @@ import {
DEFAULT_APP_SETTINGS,
DEFAULT_CLIENT_SETTINGS,
DEFAULT_CODE_FONT_SIZE,
DEFAULT_CONTENT_FONT_SIZE,
DEFAULT_UI_BASE_FONT_SIZE,
defaultUiBaseFontSize,
defaultContentFontSize,
loadAppSettingsFromStorage,
loadSettingsFromStorage,
parseClampedFontSize,
@@ -231,10 +233,45 @@ describe("loadAppSettingsFromStorage", () => {
expect(result).toEqual({
...DEFAULT_CLIENT_SETTINGS,
theme: "dark",
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
});
expect(deps.storage.entries.get(APP_SETTINGS_KEY)).toBe(JSON.stringify(result));
});
it("preserves the legacy key's explicit interface size as content size", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
[LEGACY_SETTINGS_KEY]: JSON.stringify({ uiBaseFontSize: 17 }),
}),
});
const result = await loadAppSettingsFromStorage(deps);
expect(result.uiBaseFontSize).toBe(17);
expect(result.contentFontSize).toBe(17);
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toMatchObject({
uiBaseFontSize: 17,
contentFontSize: 17,
});
});
it("preserves the legacy interface scale as content size", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
[LEGACY_SETTINGS_KEY]: JSON.stringify({ uiFontSize: 17 }),
}),
});
const result = await loadAppSettingsFromStorage(deps);
expect(result.uiBaseFontSize).toBe(15);
expect(result.contentFontSize).toBe(15);
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toMatchObject({
uiBaseFontSize: 15,
contentFontSize: 15,
});
});
it("loads a persisted explicit language", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
@@ -292,6 +329,7 @@ describe("loadSettingsFromStorage", () => {
expect(result).toEqual({
...DEFAULT_APP_SETTINGS,
theme: "light",
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
});
});
@@ -335,6 +373,7 @@ describe("loadSettingsFromStorage", () => {
expect(result).toEqual({
...DEFAULT_APP_SETTINGS,
theme: "light",
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
manageBuiltInDaemon: false,
releaseChannel: "beta",
});
@@ -355,6 +394,7 @@ describe("loadSettingsFromStorage", () => {
expect(result).toEqual({
...DEFAULT_APP_SETTINGS,
theme: "light",
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
});
});
});
@@ -399,6 +439,7 @@ describe("saveAppSettings", () => {
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toEqual({
...DEFAULT_CLIENT_SETTINGS,
theme: "light",
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
toolCallDetailLevel: "overview",
});
});
@@ -451,6 +492,7 @@ describe("appearance settings", () => {
expect(result.uiFontFamily).toBe("");
expect(result.monoFontFamily).toBe("");
expect(result.uiBaseFontSize).toBe(DEFAULT_UI_BASE_FONT_SIZE);
expect(result.contentFontSize).toBe(DEFAULT_UI_BASE_FONT_SIZE);
expect(result.codeFontSize).toBe(DEFAULT_CODE_FONT_SIZE);
expect(result.syntaxTheme).toBe("one");
expect(result.toolCallDetailLevel).toBe("detailed");
@@ -504,6 +546,51 @@ describe("appearance settings", () => {
expect(defaultUiBaseFontSize(false)).toBe(14);
});
it("uses a 15px content default on mobile and web", () => {
expect(defaultContentFontSize(true)).toBe(15);
expect(defaultContentFontSize(false)).toBe(15);
expect(DEFAULT_CONTENT_FONT_SIZE).toBe(defaultContentFontSize(false));
});
it("derives and persists content size from an existing interface-size preference", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
[APP_SETTINGS_KEY]: JSON.stringify({ uiBaseFontSize: 17 }),
}),
});
const result = await loadAppSettingsFromStorage(deps);
const persisted = JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null");
expect(result.contentFontSize).toBe(17);
expect(persisted).toMatchObject({ uiBaseFontSize: 17, contentFontSize: 17 });
});
it("clamps the content font size into range and rejects non-numeric values", async () => {
const high = makeDeps({
storage: createInMemoryKeyValueStorage({
[APP_SETTINGS_KEY]: JSON.stringify({ contentFontSize: 999 }),
}),
});
expect((await loadAppSettingsFromStorage(high)).contentFontSize).toBe(21);
const low = makeDeps({
storage: createInMemoryKeyValueStorage({
[APP_SETTINGS_KEY]: JSON.stringify({ contentFontSize: 8 }),
}),
});
expect((await loadAppSettingsFromStorage(low)).contentFontSize).toBe(10);
const bogus = makeDeps({
storage: createInMemoryKeyValueStorage({
[APP_SETTINGS_KEY]: JSON.stringify({ contentFontSize: "abc" }),
}),
});
expect((await loadAppSettingsFromStorage(bogus)).contentFontSize).toBe(
DEFAULT_CONTENT_FONT_SIZE,
);
});
it.each([
{ legacySize: 16, baseSize: 14 },
{ legacySize: 17, baseSize: 15 },
+31 -1
View File
@@ -62,6 +62,13 @@ export function defaultUiBaseFontSize(native: boolean): number {
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;
}
export const DEFAULT_CONTENT_FONT_SIZE = defaultContentFontSize(isNative);
export const MIN_CONTENT_FONT_SIZE = 10;
export const MAX_CONTENT_FONT_SIZE = 21;
export const DEFAULT_CODE_FONT_SIZE = 12; // == FONT_SIZE.code
export const MIN_CODE_FONT_SIZE = 9;
export const MAX_CODE_FONT_SIZE = 22; // line-height 1.5×22=33 stays safe
@@ -79,6 +86,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
codeFontSize: number; // clamped px, default 12
syntaxTheme: SyntaxThemeId; // default "one"
workspaceTitleSource: WorkspaceTitleSource;
@@ -126,6 +134,7 @@ const StoredAppSettingsSchema = z.strictObject({
uiFontFamily: z.string().optional(),
monoFontFamily: z.string().optional(),
uiBaseFontSize: z.union([z.number(), z.string()]).optional(),
contentFontSize: z.union([z.number(), z.string()]).optional(),
// COMPAT(uiFontSizeScale): replaced by the literal base size in v0.4, remove after 2027-08-17.
uiFontSize: z.union([z.number(), z.string()]).optional(),
codeFontSize: z.union([z.number(), z.string()]).optional(),
@@ -160,6 +169,7 @@ export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
uiFontFamily: "",
monoFontFamily: "",
uiBaseFontSize: DEFAULT_UI_BASE_FONT_SIZE,
contentFontSize: DEFAULT_CONTENT_FONT_SIZE,
codeFontSize: DEFAULT_CODE_FONT_SIZE,
syntaxTheme: "one",
workspaceTitleSource: "title",
@@ -238,7 +248,9 @@ async function readAppSettings(
return {
settings: normalizeAppSettings(stored),
// COMPAT(uiFontSizeScale): persist the converted base size, remove after 2027-08-17.
needsWrite: stored.uiBaseFontSize === undefined && stored.uiFontSize !== undefined,
needsWrite:
(stored.uiBaseFontSize === undefined && stored.uiFontSize !== undefined) ||
stored.contentFontSize === undefined,
};
}
@@ -424,6 +436,17 @@ function pickAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
result.uiBaseFontSize = Math.round((FONT_SIZE.base * legacyUiFontSize) / 16);
}
}
const contentFontSize = parseClampedFontSize(stored.contentFontSize, {
min: MIN_CONTENT_FONT_SIZE,
max: MAX_CONTENT_FONT_SIZE,
});
if (contentFontSize !== null) {
result.contentFontSize = contentFontSize;
} else if (stored.contentFontSize === undefined) {
// Existing content followed the interface ramp. Preserve that rendered size
// once, then persist the independent setting during the read migration.
result.contentFontSize = result.uiBaseFontSize ?? DEFAULT_UI_BASE_FONT_SIZE;
}
const codeFontSize = parseClampedFontSize(stored.codeFontSize, {
min: MIN_CODE_FONT_SIZE,
max: MAX_CODE_FONT_SIZE,
@@ -449,6 +472,13 @@ function pickAppSettingsFromLegacy(
if (legacy.theme === "dark" || legacy.theme === "light" || legacy.theme === "auto") {
result.theme = legacy.theme;
}
const legacyInterfaceSize = pickAppSettings(legacy).uiBaseFontSize;
if (legacyInterfaceSize !== undefined) {
result.uiBaseFontSize = legacyInterfaceSize;
}
// The legacy key rendered content on the interface ramp. Freeze that
// rendered value into the new independent preference during migration.
result.contentFontSize = legacyInterfaceSize ?? DEFAULT_UI_BASE_FONT_SIZE;
return result;
}
+10 -3
View File
@@ -2047,13 +2047,18 @@ export const ar: TranslationResources = {
interfaceFont: "خط الواجهة",
interfaceFontHint: "تستخدم عبر التطبيق. اتركه فارغًا للإعداد الافتراضي للنظام",
interfaceFontAccessibility: "عائلة خطوط الواجهة",
baseSize: "الحجم الأساسي",
baseSizeAccessibility: "حجم الخط الأساسي",
interfaceSize: "حجم الواجهة",
interfaceSizeHint: "يُستخدم للتنقل وعناصر التحكم والتسميات",
interfaceSizeAccessibility: "حجم خط الواجهة",
contentSize: "حجم المحتوى",
contentSizeHint: "يُستخدم لنص الدردشة وملفات Markdown المعروضة",
contentSizeAccessibility: "حجم خط المحتوى",
codeFont: "خط الكود",
codeFontHint:
"تستخدم في الكود والاختلافات والمخرجات الطرفية. اتركه فارغًا للإعداد الافتراضي للنظام",
codeFontAccessibility: "عائلة خطوط الكود",
codeSize: "حجم الكود",
codeSizeHint: "يُستخدم للكود والفروقات ومخرجات الطرفية",
codeSizeAccessibility: "حجم خط الكود",
},
syntax: {
@@ -2061,7 +2066,9 @@ export const ar: TranslationResources = {
highlightTheme: "تسليط الضوء على الموضوع",
highlightThemeHint: "ألوان التعليمات البرمجية، مستقلة عن سمة التطبيق",
highlightThemeAccessibility: "تسليط الضوء على الموضوع:{{value}}",
previewAccessibility: "معاينة مباشرة لموضوع بناء الجملة وخط التعليمات البرمجية",
previewAccessibility:
"معاينة مباشرة لطباعة المحتوى وموضوع بناء الجملة وخط التعليمات البرمجية",
previewContent: "معاينة المحتوى والرمز",
},
},
shortcuts: {
+9 -3
View File
@@ -2111,13 +2111,18 @@ export const en = {
interfaceFont: "Interface font",
interfaceFontHint: "Used across the app. Leave empty for the system default",
interfaceFontAccessibility: "Interface font family",
baseSize: "Base size",
baseSizeAccessibility: "Base font size",
interfaceSize: "Interface size",
interfaceSizeHint: "Used for navigation, controls, and labels",
interfaceSizeAccessibility: "Interface font size",
contentSize: "Content size",
contentSizeHint: "Used for chat text and rendered Markdown",
contentSizeAccessibility: "Content font size",
codeFont: "Code font",
codeFontHint:
"Used in code, diffs, and the terminal output. Leave empty for the system default",
codeFontAccessibility: "Code font family",
codeSize: "Code size",
codeSizeHint: "Used for code, diffs, and terminal output",
codeSizeAccessibility: "Code font size",
},
syntax: {
@@ -2125,7 +2130,8 @@ export const en = {
highlightTheme: "Highlight theme",
highlightThemeHint: "Colors for code, independent of the app theme",
highlightThemeAccessibility: "Highlight theme: {{value}}",
previewAccessibility: "Live preview of the syntax theme and code font",
previewAccessibility: "Live preview of content typography, syntax theme, and code font",
previewContent: "Readable content and code preview",
},
},
shortcuts: {
+10 -3
View File
@@ -2098,13 +2098,18 @@ export const es: TranslationResources = {
interfaceFontHint:
"Utilizado en toda la aplicación. Déjelo vacío para el valor predeterminado del sistema.",
interfaceFontAccessibility: "Familia de fuentes de interfaz",
baseSize: "Tamaño base",
baseSizeAccessibility: "Tamaño base de la fuente",
interfaceSize: "Tamaño de interfaz",
interfaceSizeHint: "Se usa en la navegación, los controles y las etiquetas",
interfaceSizeAccessibility: "Tamaño de fuente de interfaz",
contentSize: "Tamaño de contenido",
contentSizeHint: "Se usa en el texto del chat y el Markdown renderizado",
contentSizeAccessibility: "Tamaño de fuente de contenido",
codeFont: "Fuente de código",
codeFontHint:
"Se utiliza en código, diferencias y salida del terminal. Déjelo vacío para el valor predeterminado del sistema.",
codeFontAccessibility: "Familia de fuentes de código",
codeSize: "Tamaño del código",
codeSizeHint: "Se usa en código, diferencias y la salida del terminal",
codeSizeAccessibility: "Tamaño de fuente del código",
},
syntax: {
@@ -2112,7 +2117,9 @@ export const es: TranslationResources = {
highlightTheme: "Tema destacado",
highlightThemeHint: "Colores para el código, independientemente del tema de la aplicación.",
highlightThemeAccessibility: "Tema destacado:{{value}}",
previewAccessibility: "Vista previa en vivo del tema de sintaxis y fuente del código",
previewAccessibility:
"Vista previa en vivo de la tipografía de contenido, el tema de sintaxis y la fuente del código",
previewContent: "Vista previa de contenido y código",
},
},
shortcuts: {
+10 -3
View File
@@ -2102,13 +2102,18 @@ export const fr: TranslationResources = {
interfaceFontHint:
"Utilisé dans toute l'application. Laisser vide pour la valeur par défaut du système",
interfaceFontAccessibility: "Famille de polices d'interface",
baseSize: "Taille de base",
baseSizeAccessibility: "Taille de base de la police",
interfaceSize: "Taille de l'interface",
interfaceSizeHint: "Utilisée pour la navigation, les contrôles et les libellés",
interfaceSizeAccessibility: "Taille de police de l'interface",
contentSize: "Taille du contenu",
contentSizeHint: "Utilisée pour le texte du chat et le Markdown rendu",
contentSizeAccessibility: "Taille de police du contenu",
codeFont: "Police de code",
codeFontHint:
"Utilisé dans le code, les différences et la sortie du terminal. Laisser vide pour la valeur par défaut du système",
codeFontAccessibility: "Famille de polices de code",
codeSize: "Taille du code",
codeSizeHint: "Utilisée pour le code, les diffs et la sortie du terminal",
codeSizeAccessibility: "Taille de la police du code",
},
syntax: {
@@ -2116,7 +2121,9 @@ export const fr: TranslationResources = {
highlightTheme: "Thème de surbrillance",
highlightThemeHint: "Couleurs du code, indépendamment du thème de l'application",
highlightThemeAccessibility: "Thème phare:{{value}}",
previewAccessibility: "Aperçu en direct du thème de syntaxe et de la police de code",
previewAccessibility:
"Aperçu en direct de la typographie du contenu, du thème de syntaxe et de la police de code",
previewContent: "Aperçu du contenu et du code",
},
},
shortcuts: {
+10 -3
View File
@@ -2065,13 +2065,18 @@ export const ja: TranslationResources = {
interfaceFontHint:
"アプリ全体で使用されます。システムデフォルトにするには空のままにしてください",
interfaceFontAccessibility: "インターフェースフォントファミリー",
baseSize: "基本サイズ",
baseSizeAccessibility: "基本フォントサイズ",
interfaceSize: "インターフェースサイズ",
interfaceSizeHint: "ナビゲーション、コントロール、ラベルに使用されます",
interfaceSizeAccessibility: "インターフェースのフォントサイズ",
contentSize: "コンテンツサイズ",
contentSizeHint: "チャットテキストとレンダリングされた Markdown に使用されます",
contentSizeAccessibility: "コンテンツのフォントサイズ",
codeFont: "コードフォント",
codeFontHint:
"コード、差分、ターミナル出力で使用されます。システムデフォルトにするには空のままにしてください",
codeFontAccessibility: "コードフォントファミリー",
codeSize: "コードサイズ",
codeSizeHint: "コード、差分、ターミナル出力に使用されます",
codeSizeAccessibility: "コードフォントサイズ",
},
syntax: {
@@ -2079,7 +2084,9 @@ export const ja: TranslationResources = {
highlightTheme: "ハイライトテーマ",
highlightThemeHint: "コード用の色、アプリのテーマとは独立しています",
highlightThemeAccessibility: "ハイライトテーマ: {{value}}",
previewAccessibility: "構文ハイライトテーマとコードフォントのライブプレビュー",
previewAccessibility:
"コンテンツの文字組み、構文ハイライトテーマ、コードフォントのライブプレビュー",
previewContent: "コンテンツとコードのプレビュー",
},
},
shortcuts: {
+9 -3
View File
@@ -2058,13 +2058,18 @@ export const ko: TranslationResources = {
interfaceFont: "인터페이스 글꼴",
interfaceFontHint: "앱 전체에 사용됩니다. 시스템 기본값을 사용하려면 비워 두세요",
interfaceFontAccessibility: "인터페이스 글꼴 패밀리",
baseSize: "기본 크기",
baseSizeAccessibility: "기본 글꼴 크기",
interfaceSize: "인터페이스 크기",
interfaceSizeHint: "탐색, 컨트롤 및 레이블에 사용됩니다",
interfaceSizeAccessibility: "인터페이스 글꼴 크기",
contentSize: "콘텐츠 크기",
contentSizeHint: "채팅 텍스트와 렌더링된 Markdown에 사용됩니다",
contentSizeAccessibility: "콘텐츠 글꼴 크기",
codeFont: "코드 글꼴",
codeFontHint:
"코드, diff, 터미널 출력에 사용됩니다. 시스템 기본값을 사용하려면 비워 두세요",
codeFontAccessibility: "코드 글꼴 패밀리",
codeSize: "코드 크기",
codeSizeHint: "코드, diff 및 터미널 출력에 사용됩니다",
codeSizeAccessibility: "코드 글꼴 크기",
},
syntax: {
@@ -2072,7 +2077,8 @@ export const ko: TranslationResources = {
highlightTheme: "강조 테마",
highlightThemeHint: "앱 테마와 독립적인 코드 색상",
highlightThemeAccessibility: "강조 테마: {{value}}",
previewAccessibility: "구문 테마와 코드 글꼴의 실시간 미리보기",
previewAccessibility: "콘텐츠 타이포그래피, 구문 테마, 코드 글꼴의 실시간 미리보기",
previewContent: "콘텐츠 및 코드 미리보기",
},
},
shortcuts: {
+10 -3
View File
@@ -2081,13 +2081,18 @@ export const ptBR: TranslationResources = {
interfaceFont: "Fonte da interface",
interfaceFontHint: "Usada em todo o app. Deixe vazio para usar o padrão do sistema",
interfaceFontAccessibility: "Família da fonte da interface",
baseSize: "Tamanho base",
baseSizeAccessibility: "Tamanho base da fonte",
interfaceSize: "Tamanho da interface",
interfaceSizeHint: "Usado na navegação, nos controles e nos rótulos",
interfaceSizeAccessibility: "Tamanho da fonte da interface",
contentSize: "Tamanho do conteúdo",
contentSizeHint: "Usado no texto do chat e no Markdown renderizado",
contentSizeAccessibility: "Tamanho da fonte do conteúdo",
codeFont: "Fonte de código",
codeFontHint:
"Usada em código, diffs e saída do terminal. Deixe vazio para usar o padrão do sistema",
codeFontAccessibility: "Família da fonte de código",
codeSize: "Tamanho do código",
codeSizeHint: "Usado em código, diffs e saída do terminal",
codeSizeAccessibility: "Tamanho da fonte de código",
},
syntax: {
@@ -2095,7 +2100,9 @@ export const ptBR: TranslationResources = {
highlightTheme: "Tema de destaque",
highlightThemeHint: "Cores para código, independentes do tema do app",
highlightThemeAccessibility: "Tema de destaque: {{value}}",
previewAccessibility: "Prévia ao vivo do tema de sintaxe e da fonte de código",
previewAccessibility:
"Prévia ao vivo da tipografia de conteúdo, do tema de sintaxe e da fonte de código",
previewContent: "Prévia de conteúdo e código",
},
},
shortcuts: {
+9 -3
View File
@@ -2084,13 +2084,18 @@ export const ru: TranslationResources = {
interfaceFontHint:
"Используется во всём приложении. Оставьте поле пустым, чтобы использовать системный шрифт.",
interfaceFontAccessibility: "Семейство интерфейсных шрифтов",
baseSize: "Базовый размер",
baseSizeAccessibility: "Базовый размер шрифта",
interfaceSize: "Размер интерфейса",
interfaceSizeHint: "Используется для навигации, элементов управления и подписей",
interfaceSizeAccessibility: "Размер шрифта интерфейса",
contentSize: "Размер содержимого",
contentSizeHint: "Используется для текста чата и отображаемого Markdown",
contentSizeAccessibility: "Размер шрифта содержимого",
codeFont: "Шрифт кода",
codeFontHint:
"Используется для кода, диффов и вывода терминала. Оставьте поле пустым, чтобы использовать системный шрифт.",
codeFontAccessibility: "Семейство шрифтов кода",
codeSize: "Размер кода",
codeSizeHint: "Используется для кода, диффов и вывода терминала",
codeSizeAccessibility: "Размер шрифта кода",
},
syntax: {
@@ -2099,7 +2104,8 @@ export const ru: TranslationResources = {
highlightThemeHint: "Цвета кода, не зависящие от темы приложения",
highlightThemeAccessibility: "Тема подсветки: {{value}}",
previewAccessibility:
"Предварительный просмотр темы синтаксиса и шрифта кода в реальном времени.",
"Предварительный просмотр типографики содержимого, темы синтаксиса и шрифта кода в реальном времени.",
previewContent: "Предварительный просмотр содержимого и кода",
},
},
shortcuts: {
+9 -3
View File
@@ -2023,12 +2023,17 @@ export const zhCN: TranslationResources = {
interfaceFont: "界面字体",
interfaceFontHint: "用于整个应用。留空则使用系统默认",
interfaceFontAccessibility: "界面字体族",
baseSize: "基础字号",
baseSizeAccessibility: "基础字号",
interfaceSize: "界面字号",
interfaceSizeHint: "用于导航、控件和标签",
interfaceSizeAccessibility: "界面字体大小",
contentSize: "内容字号",
contentSizeHint: "用于聊天文本和渲染后的 Markdown",
contentSizeAccessibility: "内容字体大小",
codeFont: "代码字体",
codeFontHint: "用于代码、diff 和终端输出。留空则使用系统默认",
codeFontAccessibility: "代码字体族",
codeSize: "代码字号",
codeSizeHint: "用于代码、差异和终端输出",
codeSizeAccessibility: "代码字号",
},
syntax: {
@@ -2036,7 +2041,8 @@ export const zhCN: TranslationResources = {
highlightTheme: "高亮主题",
highlightThemeHint: "代码配色,独立于应用主题",
highlightThemeAccessibility: "高亮主题:{{value}}",
previewAccessibility: "语法主题和代码字体的实时预览",
previewAccessibility: "内容排版、语法主题和代码字体的实时预览",
previewContent: "内容和代码预览",
},
},
shortcuts: {
+4 -4
View File
@@ -601,8 +601,8 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minWidth: 0,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: theme.fontSize.base * 1.4,
fontSize: theme.fontSize.content,
lineHeight: theme.fontSize.content * 1.4,
},
commentActions: {
flexDirection: "row",
@@ -650,8 +650,8 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md,
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
fontSize: theme.fontSize.base,
lineHeight: theme.fontSize.base * 1.4,
fontSize: theme.fontSize.content,
lineHeight: theme.fontSize.content * 1.4,
textAlignVertical: "top",
...(isWeb
? {
@@ -26,6 +26,7 @@ const ZERO_WIDTH = "​";
type RowType = "context" | "add" | "remove";
interface PreviewOverrides {
contentFontSize?: number;
monoFontFamily?: string;
codeFontSize?: number;
}
@@ -63,6 +64,12 @@ function buildCodeOverride(overrides: PreviewOverrides | undefined): TextStyle {
return inlineUnistylesStyle(style);
}
function buildContentOverride(overrides: PreviewOverrides | undefined): TextStyle {
const fontSize = resolveSizeOverride(overrides?.contentFontSize);
if (fontSize === undefined) return {};
return inlineUnistylesStyle({ fontSize, lineHeight: Math.round(fontSize * 1.4) });
}
interface KeyedToken {
key: string;
style: string | null;
@@ -137,7 +144,9 @@ function markerStyle(type: RowType) {
export function AppearancePreview({ overrides }: AppearancePreviewProps) {
const { t } = useTranslation();
const rows = useMemo(() => buildUnifiedRows(), []);
const contentOverride = useMemo(() => buildContentOverride(overrides), [overrides]);
const codeOverride = useMemo(() => buildCodeOverride(overrides), [overrides]);
const contentStyle = useMemo(() => [styles.contentSample, contentOverride], [contentOverride]);
const codeStyle = useMemo(() => [styles.codeLine, codeOverride], [codeOverride]);
const addRowStyle = useMemo(() => [styles.row, styles.addRow], []);
const removeRowStyle = useMemo(() => [styles.row, styles.removeRow], []);
@@ -155,6 +164,7 @@ export function AppearancePreview({ overrides }: AppearancePreviewProps) {
dataSet={CODE_SURFACE_DATASET}
style={styles.card}
>
<Text style={contentStyle}>{t("settings.appearance.syntax.previewContent")}</Text>
{rows.map((row) => (
<View key={row.key} style={rowStyle(row.type)}>
<Text style={codeStyle}>
@@ -201,6 +211,13 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
...(isWeb ? { whiteSpace: "pre", overflowWrap: "normal" } : null),
},
contentSample: {
color: theme.colors.foreground,
fontSize: theme.fontSize.content,
lineHeight: Math.round(theme.fontSize.content * 1.4),
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[2],
},
markerContext: {
color: theme.colors.foregroundMuted,
},
@@ -22,8 +22,10 @@ import { useContributedThemes } from "@/appearance/provider";
import { EditingTextInput as TextInput } from "@/components/ui/text-input";
import {
MAX_CODE_FONT_SIZE,
MAX_CONTENT_FONT_SIZE,
MAX_UI_BASE_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_CONTENT_FONT_SIZE,
MIN_UI_BASE_FONT_SIZE,
parseClampedFontSize,
sanitizeFontFamily,
@@ -401,6 +403,7 @@ function FontFamilyRow({
interface FontSizeRowProps {
title: string;
hint: string;
accessibilityLabel: string;
draft: string;
withBorder?: boolean;
@@ -410,6 +413,7 @@ interface FontSizeRowProps {
function FontSizeRow({
title,
hint,
accessibilityLabel,
draft,
withBorder = true,
@@ -420,6 +424,7 @@ function FontSizeRow({
<View style={withBorder ? styles.rowWithBorder : settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{title}</Text>
<Text style={settingsStyles.rowHint}>{hint}</Text>
</View>
<View style={styles.sizeField}>
<TextInput
@@ -527,12 +532,16 @@ export function AppearanceSection() {
const [uiFontDraft, setUiFontDraft] = useState(settings.uiFontFamily);
const [monoFontDraft, setMonoFontDraft] = useState(settings.monoFontFamily);
const [uiBaseSizeDraft, setUiBaseSizeDraft] = useState(String(settings.uiBaseFontSize));
const [contentSizeDraft, setContentSizeDraft] = useState(String(settings.contentFontSize));
const [codeSizeDraft, setCodeSizeDraft] = useState(String(settings.codeFontSize));
// Resync numeric drafts when the committed value changes elsewhere.
useEffect(() => {
setUiBaseSizeDraft(String(settings.uiBaseFontSize));
}, [settings.uiBaseFontSize]);
useEffect(() => {
setContentSizeDraft(String(settings.contentFontSize));
}, [settings.contentFontSize]);
useEffect(() => {
setCodeSizeDraft(String(settings.codeFontSize));
}, [settings.codeFontSize]);
@@ -617,6 +626,10 @@ export function AppearanceSection() {
setCodeSizeDraft(value.replace(/[^\d]/g, ""));
}, []);
const handleContentSizeChange = useCallback((value: string) => {
setContentSizeDraft(value.replace(/[^\d]/g, ""));
}, []);
const commitUiBaseSize = useCallback(() => {
const parsed = parseClampedFontSize(uiBaseSizeDraft, {
min: MIN_UI_BASE_FONT_SIZE,
@@ -641,15 +654,28 @@ export function AppearanceSection() {
}
}, [codeSizeDraft, settings.codeFontSize, updateSettings]);
const commitContentSize = useCallback(() => {
const parsed = parseClampedFontSize(contentSizeDraft, {
min: MIN_CONTENT_FONT_SIZE,
max: MAX_CONTENT_FONT_SIZE,
});
const next = parsed ?? settings.contentFontSize;
setContentSizeDraft(String(next));
if (next !== settings.contentFontSize) {
void updateSettings({ contentFontSize: next });
}
}, [contentSizeDraft, settings.contentFontSize, updateSettings]);
// Live-while-typing: the in-progress drafts drive the preview without
// committing to the global theme. Empty/invalid fields fall back to the
// theme value inside the preview.
const previewOverrides = useMemo(
() => ({
contentFontSize: sizeDraftToOverride(contentSizeDraft),
monoFontFamily: monoFontDraft,
codeFontSize: sizeDraftToOverride(codeSizeDraft),
}),
[codeSizeDraft, monoFontDraft],
[codeSizeDraft, contentSizeDraft, monoFontDraft],
);
return (
@@ -699,13 +725,22 @@ export function AppearanceSection() {
/>
) : null}
<FontSizeRow
title={t("settings.appearance.fonts.baseSize")}
accessibilityLabel={t("settings.appearance.fonts.baseSizeAccessibility")}
title={t("settings.appearance.fonts.interfaceSize")}
hint={t("settings.appearance.fonts.interfaceSizeHint")}
accessibilityLabel={t("settings.appearance.fonts.interfaceSizeAccessibility")}
draft={uiBaseSizeDraft}
withBorder={showInterfaceFontFamilyRow}
onChangeDraft={handleUiBaseSizeChange}
onCommit={commitUiBaseSize}
/>
<FontSizeRow
title={t("settings.appearance.fonts.contentSize")}
hint={t("settings.appearance.fonts.contentSizeHint")}
accessibilityLabel={t("settings.appearance.fonts.contentSizeAccessibility")}
draft={contentSizeDraft}
onChangeDraft={handleContentSizeChange}
onCommit={commitContentSize}
/>
<FontFamilyRow
title={t("settings.appearance.fonts.codeFont")}
hint={t("settings.appearance.fonts.codeFontHint")}
@@ -719,6 +754,7 @@ export function AppearanceSection() {
/>
<FontSizeRow
title={t("settings.appearance.fonts.codeSize")}
hint={t("settings.appearance.fonts.codeSizeHint")}
accessibilityLabel={t("settings.appearance.fonts.codeSizeAccessibility")}
draft={codeSizeDraft}
onChangeDraft={handleCodeSizeChange}
@@ -3,20 +3,20 @@ import { createCompactMarkdownStyles, createMarkdownStyles } from "./markdown-st
import { darkTheme } from "./theme";
describe("createMarkdownStyles", () => {
it("uses the standard interface size for conversation prose and list markers", () => {
it("uses the content size for conversation prose and list markers", () => {
const styles = createMarkdownStyles(darkTheme);
const proseLineHeight = Math.round(darkTheme.fontSize.base * 1.4);
const proseLineHeight = Math.round(darkTheme.fontSize.content * 1.4);
expect(styles.body).toMatchObject({
fontSize: darkTheme.fontSize.base,
fontSize: darkTheme.fontSize.content,
lineHeight: proseLineHeight,
});
expect(styles.bullet_list_icon).toMatchObject({
fontSize: darkTheme.fontSize.base,
fontSize: darkTheme.fontSize.content,
lineHeight: proseLineHeight,
});
expect(styles.ordered_list_icon).toMatchObject({
fontSize: darkTheme.fontSize.base,
fontSize: darkTheme.fontSize.content,
lineHeight: proseLineHeight,
});
});
@@ -112,6 +112,18 @@ describe("createMarkdownStyles", () => {
expect(compactStyles.code_inline).not.toHaveProperty("lineHeight");
});
it("scales Markdown headings from content size with safe line heights", () => {
const largeContentTheme = {
...darkTheme,
fontSize: { ...darkTheme.fontSize, content: 21 },
};
const styles = createMarkdownStyles(largeContentTheme);
expect(styles.heading1.lineHeight).toBeGreaterThan(styles.heading1.fontSize);
expect(styles.heading2.lineHeight).toBeGreaterThan(styles.heading2.fontSize);
expect(styles.heading3.lineHeight).toBeGreaterThan(styles.heading3.fontSize);
});
it("keeps blockquotes quiet with a square left edge", () => {
const styles = createMarkdownStyles(darkTheme);
+38 -30
View File
@@ -1,8 +1,16 @@
import type { Theme } from "./theme";
import { FONT_SIZE, type Theme } from "./theme";
import { isWeb } from "@/constants/platform";
const webSelectableTextStyle = isWeb ? { userSelect: "text" as const } : {};
function contentHeadingSize(contentSize: number, tier: keyof typeof FONT_SIZE): number {
return Math.round(contentSize * (FONT_SIZE[tier] / FONT_SIZE.base));
}
function contentHeadingLineHeight(contentSize: number, tier: keyof typeof FONT_SIZE): number {
return Math.round(contentHeadingSize(contentSize, tier) * 1.3);
}
/**
* Creates comprehensive markdown styles for react-native-markdown-display.
*
@@ -19,10 +27,10 @@ export function createMarkdownStyles(theme: Theme) {
body: {
...webSelectableTextStyle,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
// Prose line-height scales with the UI ramp, not the
fontSize: theme.fontSize.content,
// Prose line-height scales with the content size, not the
// code-size-coupled lineHeight.diff token used by code/diff surfaces.
lineHeight: Math.round(theme.fontSize.base * 1.4),
lineHeight: Math.round(theme.fontSize.content * 1.4),
flexShrink: 1,
minWidth: 0,
width: "100%" as const,
@@ -53,12 +61,12 @@ export function createMarkdownStyles(theme: Theme) {
heading1: {
...webSelectableTextStyle,
fontSize: theme.fontSize["4xl"],
fontSize: contentHeadingSize(theme.fontSize.content, "4xl"),
fontWeight: theme.fontWeight.bold,
color: theme.colors.foreground,
marginTop: theme.spacing[6],
marginBottom: theme.spacing[3],
lineHeight: 32,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "4xl"),
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
paddingBottom: theme.spacing[2],
@@ -66,12 +74,12 @@ export function createMarkdownStyles(theme: Theme) {
heading2: {
...webSelectableTextStyle,
fontSize: theme.fontSize["3xl"],
fontSize: contentHeadingSize(theme.fontSize.content, "3xl"),
fontWeight: theme.fontWeight.bold,
color: theme.colors.foreground,
marginTop: theme.spacing[6],
marginBottom: theme.spacing[3],
lineHeight: 28,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "3xl"),
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
paddingBottom: theme.spacing[2],
@@ -79,42 +87,42 @@ export function createMarkdownStyles(theme: Theme) {
heading3: {
...webSelectableTextStyle,
fontSize: theme.fontSize["2xl"],
fontSize: contentHeadingSize(theme.fontSize.content, "2xl"),
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 26,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"),
},
heading4: {
...webSelectableTextStyle,
fontSize: theme.fontSize.xl,
fontSize: contentHeadingSize(theme.fontSize.content, "xl"),
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 24,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"),
},
heading5: {
...webSelectableTextStyle,
fontSize: theme.fontSize.lg,
fontSize: contentHeadingSize(theme.fontSize.content, "lg"),
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 22,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"),
},
heading6: {
...webSelectableTextStyle,
fontSize: theme.fontSize.lg,
fontSize: contentHeadingSize(theme.fontSize.content, "lg"),
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foregroundMuted,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 20,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"),
textTransform: "uppercase" as const,
letterSpacing: 0.5,
},
@@ -227,7 +235,7 @@ export function createMarkdownStyles(theme: Theme) {
backgroundColor: theme.colors.surface2,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.content,
textAlign: "left" as const,
},
@@ -243,7 +251,7 @@ export function createMarkdownStyles(theme: Theme) {
borderRightWidth: 1,
borderColor: theme.colors.border,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.content,
flex: 1,
},
@@ -282,17 +290,17 @@ export function createMarkdownStyles(theme: Theme) {
...webSelectableTextStyle,
color: theme.colors.foregroundMuted,
marginRight: 4,
fontSize: theme.fontSize.base,
lineHeight: Math.round(theme.fontSize.base * 1.4),
fontSize: theme.fontSize.content,
lineHeight: Math.round(theme.fontSize.content * 1.4),
},
ordered_list_icon: {
...webSelectableTextStyle,
color: theme.colors.foregroundMuted,
marginRight: 4,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.content,
fontWeight: theme.fontWeight.normal,
lineHeight: Math.round(theme.fontSize.base * 1.4),
lineHeight: Math.round(theme.fontSize.content * 1.4),
minWidth: 12,
},
@@ -357,32 +365,32 @@ export function createCompactMarkdownStyles(theme: Theme) {
body: {
...baseStyles.body,
fontSize: theme.fontSize.base,
lineHeight: 20,
fontSize: theme.fontSize.content,
lineHeight: Math.round(theme.fontSize.content * 1.4),
},
heading1: {
...baseStyles.heading1,
fontSize: theme.fontSize["2xl"],
fontSize: contentHeadingSize(theme.fontSize.content, "2xl"),
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 26,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"),
},
heading2: {
...baseStyles.heading2,
fontSize: theme.fontSize.xl,
fontSize: contentHeadingSize(theme.fontSize.content, "xl"),
marginTop: theme.spacing[3],
marginBottom: theme.spacing[2],
lineHeight: 24,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"),
},
heading3: {
...baseStyles.heading3,
fontSize: theme.fontSize.lg,
fontSize: contentHeadingSize(theme.fontSize.content, "lg"),
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 22,
lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"),
},
paragraph: {
+1
View File
@@ -12,6 +12,7 @@ describe("Typography scale", () => {
it("names 14px as the default interface tier", () => {
expect(FONT_SIZE).toEqual({
code: 12,
content: 15,
sm: 12,
base: 14,
lg: 16,
+1
View File
@@ -558,6 +558,7 @@ export const SPACING = {
export const FONT_SIZE = {
code: 12,
content: 15,
sm: 12,
base: 14,
lg: 16,
@@ -15,29 +15,33 @@ export async function runAppearanceFontSizeRegression(page) {
await page.getByLabel("Theme: System", { exact: true }).click();
await page.getByText("Pure black", { exact: true }).click();
const input = page.getByRole("textbox", { name: "Base font size" });
const interfaceSizeInput = page.getByRole("textbox", { name: "Interface font size" });
const contentSizeInput = page.getByRole("textbox", { name: "Content font size" });
const sectionTitle = page.getByText("Theme", { exact: true }).first();
await input.waitFor({ state: "visible", timeout: SETTINGS_TIMEOUT_MS });
await interfaceSizeInput.waitFor({ state: "visible", timeout: SETTINGS_TIMEOUT_MS });
assert((await input.inputValue()) === "14", "Base font size did not start at 14px");
assert((await interfaceSizeInput.inputValue()) === "14", "Interface size did not start at 14px");
assert((await contentSizeInput.inputValue()) === "15", "Content size did not start at 15px");
assert(
(await readFontSize(sectionTitle)) === "12px",
"Theme label did not start at the default 12px ramp size",
);
await input.fill("15");
await input.press("Tab");
await interfaceSizeInput.fill("12");
await interfaceSizeInput.press("Tab");
await page.waitForFunction(
() => {
const inputElement = document.querySelector('input[aria-label="Base font size"]');
const interfaceInput = document.querySelector('input[aria-label="Interface font size"]');
const contentInput = document.querySelector('input[aria-label="Content font size"]');
const themeLabel = [...document.querySelectorAll("div")].find(
(element) => element.children.length === 0 && element.textContent?.trim() === "Theme",
);
return (
inputElement?.value === "15" &&
interfaceInput?.value === "12" &&
contentInput?.value === "15" &&
themeLabel instanceof HTMLElement &&
getComputedStyle(themeLabel).fontSize === "13px"
getComputedStyle(themeLabel).fontSize === "10px"
);
},
undefined,