* 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
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import React from "react";
|
||
import { Text, View } from "react-native";
|
||
import { StyleSheet } from "react-native-unistyles";
|
||
import { isWeb } from "@/constants/platform";
|
||
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
|
||
import { syntaxTokenStyleFor } from "@/styles/syntax-token-styles";
|
||
import type { KeyedLine, KeyedToken } from "@/utils/highlight-cache";
|
||
|
||
// Line height kept in sync with the plain `scrollText` style in
|
||
// tool-call-details so a highlighted block lines up with an unhighlighted
|
||
// fallback and blank lines keep their height.
|
||
const CODE_LINE_HEIGHT = 18;
|
||
const ZERO_WIDTH = "";
|
||
|
||
interface HighlightedLinesProps {
|
||
lines: KeyedLine[];
|
||
// 1-based line number of the first line; when set, a line-number gutter is
|
||
// rendered (used by Read, which carries a server-normalized offset).
|
||
startLine?: number;
|
||
}
|
||
|
||
function ContentLine({ line }: { line: KeyedLine }) {
|
||
return (
|
||
<Text selectable style={styles.lineText}>
|
||
{line.tokens.length === 0
|
||
? ZERO_WIDTH
|
||
: line.tokens.map(({ key, token }: KeyedToken) => (
|
||
<Text key={key} style={token.style ? syntaxTokenStyleFor(token.style) : undefined}>
|
||
{token.text}
|
||
</Text>
|
||
))}
|
||
</Text>
|
||
);
|
||
}
|
||
|
||
const GutteredLine = React.memo(function GutteredLine({
|
||
line,
|
||
lineNumber,
|
||
digits,
|
||
}: {
|
||
line: KeyedLine;
|
||
lineNumber: number;
|
||
digits: number;
|
||
}) {
|
||
return (
|
||
<View style={styles.row}>
|
||
<Text style={styles.gutterText}>{String(lineNumber).padStart(digits)} </Text>
|
||
<ContentLine line={line} />
|
||
</View>
|
||
);
|
||
});
|
||
|
||
// Renders pre-tokenized lines (from the shared highlight cache), optionally with
|
||
// a line-number gutter. Callers decide whether to highlight at all, so the
|
||
// expensive size-cap / unsupported-language fallback stays a single plain Text.
|
||
export function HighlightedLines({ lines, startLine }: HighlightedLinesProps) {
|
||
if (startLine === undefined) {
|
||
return (
|
||
<View dataSet={CODE_SURFACE_DATASET}>
|
||
{lines.map((line) => (
|
||
<ContentLine key={line.key} line={line} />
|
||
))}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const lastLineNumber = startLine + lines.length - 1;
|
||
const digits = Math.max(2, String(lastLineNumber).length);
|
||
return (
|
||
<View dataSet={CODE_SURFACE_DATASET}>
|
||
{lines.map((line, index) => (
|
||
<GutteredLine key={line.key} line={line} lineNumber={startLine + index} digits={digits} />
|
||
))}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create((theme) => ({
|
||
row: {
|
||
flexDirection: "row",
|
||
minHeight: CODE_LINE_HEIGHT,
|
||
},
|
||
gutterText: {
|
||
fontFamily: theme.fontFamily.mono,
|
||
fontSize: theme.fontSize.code,
|
||
lineHeight: CODE_LINE_HEIGHT,
|
||
color: theme.colors.foregroundMuted,
|
||
opacity: 0.6,
|
||
userSelect: "none",
|
||
flexShrink: 0,
|
||
},
|
||
lineText: {
|
||
fontFamily: theme.fontFamily.mono,
|
||
fontSize: theme.fontSize.code,
|
||
color: theme.colors.foreground,
|
||
lineHeight: CODE_LINE_HEIGHT,
|
||
...(isWeb
|
||
? {
|
||
whiteSpace: "pre",
|
||
overflowWrap: "normal",
|
||
}
|
||
: null),
|
||
},
|
||
}));
|