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:
+1
-1
@@ -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 ramp uses a 14px base. New native installs default to 15px; web and desktop default to 14px. The Appearance **Base size** setting is the rendered `fontSize.base` value and scales the rest of the UI ramp proportionally. 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 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.
|
||||
|
||||
Weight has three tiers, applied by role:
|
||||
|
||||
|
||||
+1
-1
@@ -344,7 +344,7 @@ Gotchas:
|
||||
- **Patch all themes, not just the active one.** The active theme can change and adaptive mode can flip light/dark; patching every key keeps the active key current and makes ordering vs `setTheme`/`setAdaptiveThemes` irrelevant. The effect depends on the settings values (not on `theme`), so it cannot loop.
|
||||
- **The reserved plugin keys are the exception to that ordering.** A plugin-contributed theme is rebuilt from its palette, which discards the appearance patch, so the appearance provider writes the matching light or dark slot before applying font and syntax preferences. See [plugins.md](plugins.md).
|
||||
- **Narrow the discriminated union before spreading.** `updateTheme`'s updater returns the theme union; spreading the union widens `colorScheme` to `"light" | "dark"`, which is assignable to neither concrete member. Branch on `t.colorScheme` so each branch spreads a single narrowed theme type (no `as`).
|
||||
- **`lineHeight.diff` is the code/diff line-height axis** — it is coupled to the code-font-size control (≈ `codeFontSize * 1.5`). Do NOT use it for prose. Markdown body line-height scales with the UI ramp (`Math.round(theme.fontSize.base * 1.4)`); routing prose through `lineHeight.diff` clips text at small code sizes.
|
||||
- **`lineHeight.diff` is the code/diff line-height axis** — it is coupled to the code-font-size control (≈ `codeFontSize * 1.5`). Do NOT use it for prose. Markdown body line-height scales with content size (`Math.round(theme.fontSize.content * 1.4)`); routing prose through `lineHeight.diff` clips text at small code sizes.
|
||||
- **High-churn draft values** (live-while-typing in the appearance preview) bypass the theme: apply them as inline styles marked with `inlineUnistylesStyle` so per-keystroke values don't grow the `#unistyles-web` CSS registry.
|
||||
- **The app shell uses one `AppearanceStyleBoundary`.** Runtime-patched numeric theme values are baked into Unistyles web classes rather than CSS variables, while parsed/memoized content also does not naturally re-run when appearance tokens change. The boundary sits below stable runtime providers in `app/_layout.tsx` and remounts the visual shell once. `applyAppearance` patches the active theme before inactive registry entries so its subscribers receive the committed values in the same update. Do not add local appearance keys or nested boundaries.
|
||||
- **Dynamic font tokens stay widened.** `fontFamily`, `fontSize`, and `lineHeight` on `commonTheme` are annotated `string`/`number` (not narrowed by `as const`) so the updater's return assigns; the platform default stacks live in `DEFAULT_UI_FONT_STACK` / `DEFAULT_MONO_FONT_STACK`.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user