Add appearance settings for theme, fonts, and syntax highlighting (#1236)

* Add appearance settings for theme, fonts, and syntax highlighting

New Appearance section in Settings to configure the app theme, UI and
mono font family + size, and a syntax-highlighting theme chosen
independently of light/dark. Font family fields default to empty and
show the active system stack as the placeholder; a live split-diff
preview reflects the choices as they change. The theme picker moves
here from General.

Preferences are client-only (AsyncStorage) and applied at runtime by
patching every registered Unistyles theme via
UnistylesRuntime.updateTheme, so existing StyleSheet consumers repaint
with no per-component preference reads and no useUnistyles. No
daemon/protocol change.

* Expand syntax themes and move the preview under the chooser

Replace the small fixed syntax-theme set with eight popular themes — GitHub,
Catppuccin, Dracula, Tokyo Night, One, Nord, Gruvbox, Solarized — built from a
compact per-theme role palette. The app theme and syntax theme stay separate;
the only coupling is the light/dark axis: a theme with both variants uses its
light palette on a light app and its dark palette on a dark app, while the code
frame (gutter, line numbers, background) keeps following the app theme. Default
is now GitHub (GitHub keeps its existing hand-tuned maps for an exact match).

Move the live preview directly under the syntax-theme chooser, drop the
"preview.ts" filename header, and use a clearer code snippet for it.

* Update settings e2e for the moved theme picker and Appearance section

The theme picker moved from General to the new Appearance section, so the
General-content assertion no longer finds "Theme" — point it at "Default send"
(which stays in General) and add Appearance to the e2e section map plus an
expectAppearanceContent check, and exercise the Appearance section in the
sidebar-navigation test.

* Render the appearance preview as a unified diff

Switch the live preview from side-by-side columns to a single unified diff:
unchanged context lines plus "-"/"+" rows for the change, matching how diffs
read elsewhere in the app. Same snippet, mono font, syntax colors, and live
code-font draft behavior.

* Fix code font size change shrinking all UI text

applyAppearance scaled the live (already-patched) theme ramp instead of the
authored FONT_SIZE ramp, so it was not idempotent: every appearance change
re-scaled the current sizes, compounding the whole-UI font scale. With any
non-default UI size, changing the code size — or any setting — shrank (or grew)
all app text, and it persisted into the theme. Build the ramp from the canonical
FONT_SIZE ramp each apply (code size still set absolutely), so applies are
idempotent and a code-size change never touches the UI ramp.

* Apply the interface font across the app on web

react-native-web stamps a default font onto every text element, so the interface
font only reached the few components that set a font explicitly. Inject one rule
that points all text at a CSS variable (updated live as the setting changes),
with high specificity (1,2,0) so it deterministically beats RN-web's base font
and Unistyles' generated classes — no stylesheet-order fragility. Code, diff, and
monospace surfaces carry a `data-pmono` marker (and their subtrees are excluded),
so they keep their code font.

Web-only: React Native has no global font cascade, so native still applies the UI
font only where components opt in. Inline `code` within chat prose is a known
minor gap (its render paths aren't tagged yet).

* Keep preview code in the code font

* Apply code font settings to terminals

* Fix settings host picker import after rebase

* Format terminal webview bundle
This commit is contained in:
Mohamed Boudra
2026-05-31 16:09:01 +08:00
committed by GitHub
parent f26a81798b
commit a1c8e1a1f9
43 changed files with 2083 additions and 247 deletions
+7
View File
@@ -0,0 +1,7 @@
// Marker for code / diff / monospace surfaces. On web, the app-wide interface-font
// rule (see screens/settings/appearance/apply-root-font.web.ts) targets
// `#root *:not([data-pmono]):not([data-pmono] *)`, so tagging a code container with
// this dataSet excludes it and its subtree — they keep their monospace font. On
// native it renders nothing and is harmless. Use a shared stable reference so it
// doesn't trip the react-perf "new object as prop" rule.
export const CODE_SURFACE_DATASET = { pmono: "" } as const;
+7 -6
View File
@@ -1,5 +1,4 @@
import type { Theme } from "./theme";
import { Fonts } from "@/constants/theme";
import { isWeb } from "@/constants/platform";
const webSelectableTextStyle = isWeb ? { userSelect: "text" as const } : {};
@@ -21,7 +20,9 @@ export function createMarkdownStyles(theme: Theme) {
...webSelectableTextStyle,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 22,
// Prose line-height scales with the UI ramp (≈22 at base 16), NOT the
// code-size-coupled lineHeight.diff token used by code/diff surfaces.
lineHeight: Math.round(theme.fontSize.base * 1.4),
flexShrink: 1,
minWidth: 0,
width: "100%" as const,
@@ -169,8 +170,8 @@ export function createMarkdownStyles(theme: Theme) {
paddingVertical: 2,
borderRadius: theme.borderRadius.md,
borderWidth: 0,
fontFamily: Fonts.mono,
fontSize: theme.fontSize.base - 3,
fontFamily: theme.fontFamily.mono,
fontSize: theme.fontSize.code,
},
code_block: {
@@ -179,7 +180,7 @@ export function createMarkdownStyles(theme: Theme) {
color: theme.colors.foreground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: Fonts.mono,
fontFamily: theme.fontFamily.mono,
fontSize: theme.fontSize.code,
marginVertical: theme.spacing[2],
},
@@ -192,7 +193,7 @@ export function createMarkdownStyles(theme: Theme) {
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.border,
fontFamily: Fonts.mono,
fontFamily: theme.fontFamily.mono,
fontSize: theme.fontSize.code,
marginVertical: theme.spacing[3],
},
+35 -2
View File
@@ -1,3 +1,4 @@
import { Platform } from "react-native";
import { darkHighlightColors, lightHighlightColors } from "@getpaseo/highlight";
export const baseColors = {
@@ -485,16 +486,48 @@ export const OPACITY = {
100: 1,
} as const;
const commonTheme = {
// Platform default font stacks — copied verbatim from constants/theme.ts `Fonts`
// (sans -> ui, mono -> mono). These seed the dynamic `fontFamily` theme token and
// are the fallback an empty user-supplied family resolves to at apply time.
export const DEFAULT_UI_FONT_STACK: string = Platform.select({
ios: "system-ui",
default: "normal",
web: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
});
export const DEFAULT_MONO_FONT_STACK: string = Platform.select({
ios: "ui-monospace",
default: "monospace",
web: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
});
// `fontSize`, `fontFamily`, and `lineHeight` are deliberately widened to plain
// `number`/`string` (not narrowed by `as const`) so the appearance updater can patch
// them at runtime via `UnistylesRuntime.updateTheme`. The remaining tokens keep their
// literal types.
interface CommonTheme {
spacing: typeof SPACING;
fontSize: Record<keyof typeof FONT_SIZE, number>;
fontFamily: { ui: string; mono: string };
lineHeight: Record<keyof typeof LINE_HEIGHT, number>;
iconSize: typeof ICON_SIZE;
fontWeight: typeof FONT_WEIGHT;
borderRadius: typeof BORDER_RADIUS;
borderWidth: typeof BORDER_WIDTH;
opacity: typeof OPACITY;
}
const commonTheme: CommonTheme = {
spacing: SPACING,
fontSize: FONT_SIZE,
fontFamily: { ui: DEFAULT_UI_FONT_STACK, mono: DEFAULT_MONO_FONT_STACK },
lineHeight: LINE_HEIGHT,
iconSize: ICON_SIZE,
fontWeight: FONT_WEIGHT,
borderRadius: BORDER_RADIUS,
borderWidth: BORDER_WIDTH,
opacity: OPACITY,
} as const;
};
const darkShadow = {
sm: {