Files
paseo/packages/app/src/appearance/apply.ts
T
Mohamed Boudra 602860625c 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.
2026-08-21 12:35:23 +02:00

106 lines
4.0 KiB
TypeScript

import { UnistylesRuntime } from "react-native-unistyles";
import { resolveSyntaxColors, type SyntaxThemeId } from "@getpaseo/highlight";
import {
DEFAULT_UI_FONT_STACK,
DEFAULT_MONO_FONT_STACK,
FONT_SIZE,
REGISTERED_THEMES,
type Theme,
} from "@/styles/theme";
import { applyRootUiFont } from "./apply-root-font";
const ALL_THEME_KEYS = Object.keys(REGISTERED_THEMES) as (keyof typeof REGISTERED_THEMES)[];
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;
}
/**
* Build the font-size ramp from the canonical `FONT_SIZE` ramp, scaled
* proportionally from the requested base size so the type hierarchy is preserved
* sizes. Deriving from the authored ramp — NOT the live (possibly already-scaled)
* theme — makes `applyAppearance` idempotent: repeated applies never compound, and a
* code-size change (base size unchanged) leaves the UI ramp at its authored values.
* `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,
contentSize: number,
codeSize: number,
): Theme["fontSize"] {
const r = uiBaseSize / FONT_SIZE.base;
return {
sm: Math.round(FONT_SIZE.sm * r),
base: Math.round(FONT_SIZE.base * r),
lg: Math.round(FONT_SIZE.lg * r),
xl: Math.round(FONT_SIZE.xl * r),
"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
};
}
/**
* Patch every registered Unistyles theme with the user's appearance choices.
* All keys in `ALL_THEME_KEYS` are patched because the active theme can change
* and adaptive mode can flip light/dark — patching all keys keeps the active key
* always current and makes ordering vs `setTheme`/`setAdaptiveThemes` irrelevant.
*
* The updater preserves the active theme wholesale (surfaces, accents,
* terminal) and only patches the font ramp and syntax palette.
* `updateTheme` replaces the stored theme rather than merging, so we spread
* `...t` first.
*/
export function applyAppearance(input: AppearanceInput): void {
const ui = input.uiFontFamily.trim() || DEFAULT_UI_FONT_STACK;
const mono = input.monoFontFamily.trim() || DEFAULT_MONO_FONT_STACK;
const diffLineHeight = Math.round(input.codeFontSize * 1.5); // couple to code size
const activeTheme = UnistylesRuntime.themeName;
// Unistyles web emits after each registry patch. Updating the mounted theme
// first ensures subscribers receive its new numeric tokens in this render;
// updating it last makes Pure black appear one committed value behind.
const themeKeys = activeTheme
? [activeTheme, ...ALL_THEME_KEYS.filter((key) => key !== activeTheme)]
: ALL_THEME_KEYS;
for (const key of themeKeys) {
UnistylesRuntime.updateTheme(key, (t) => {
const fontFamily = { ui, mono };
const fontSize = scaleFontSize(
input.uiBaseFontSize,
input.contentFontSize,
input.codeFontSize,
);
const lineHeight = { ...t.lineHeight, diff: diffLineHeight };
if (t.colorScheme === "light") {
return {
...t,
fontFamily,
fontSize,
lineHeight,
colors: { ...t.colors, syntax: resolveSyntaxColors(input.syntaxTheme, t.colorScheme) },
};
}
return {
...t,
fontFamily,
fontSize,
lineHeight,
colors: { ...t.colors, syntax: resolveSyntaxColors(input.syntaxTheme, t.colorScheme) },
};
});
}
// Web: apply the UI font app-wide (RN-web stamps a default font on every text
// element, so it can't be done through the theme alone). No-op on native.
applyRootUiFont(ui);
}