* 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
408 lines
11 KiB
TypeScript
408 lines
11 KiB
TypeScript
import type { Theme } from "./theme";
|
|
import { isWeb } from "@/constants/platform";
|
|
|
|
const webSelectableTextStyle = isWeb ? { userSelect: "text" as const } : {};
|
|
|
|
/**
|
|
* Creates comprehensive markdown styles for react-native-markdown-display.
|
|
*
|
|
* Usage:
|
|
* const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
|
* <Markdown style={markdownStyles}>{content}</Markdown>
|
|
*/
|
|
export function createMarkdownStyles(theme: Theme) {
|
|
return {
|
|
// =========================================================================
|
|
// BASE STYLES
|
|
// =========================================================================
|
|
|
|
body: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.base,
|
|
// 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,
|
|
},
|
|
|
|
text: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.foreground,
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
overflowWrap: "anywhere" as const,
|
|
},
|
|
|
|
paragraph: {
|
|
marginTop: 0,
|
|
marginBottom: theme.spacing[3],
|
|
flexWrap: "wrap" as const,
|
|
flexDirection: "row" as const,
|
|
alignItems: "flex-start" as const,
|
|
justifyContent: "flex-start" as const,
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
width: "100%" as const,
|
|
},
|
|
|
|
// =========================================================================
|
|
// HEADINGS
|
|
// =========================================================================
|
|
|
|
heading1: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize["3xl"],
|
|
fontWeight: theme.fontWeight.bold,
|
|
color: theme.colors.foreground,
|
|
marginTop: theme.spacing[6],
|
|
marginBottom: theme.spacing[3],
|
|
lineHeight: 32,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
paddingBottom: theme.spacing[2],
|
|
},
|
|
|
|
heading2: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize["2xl"],
|
|
fontWeight: theme.fontWeight.bold,
|
|
color: theme.colors.foreground,
|
|
marginTop: theme.spacing[6],
|
|
marginBottom: theme.spacing[3],
|
|
lineHeight: 28,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: theme.colors.border,
|
|
paddingBottom: theme.spacing[2],
|
|
},
|
|
|
|
heading3: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize.xl,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
marginTop: theme.spacing[4],
|
|
marginBottom: theme.spacing[2],
|
|
lineHeight: 26,
|
|
},
|
|
|
|
heading4: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize.lg,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
marginTop: theme.spacing[4],
|
|
marginBottom: theme.spacing[2],
|
|
lineHeight: 24,
|
|
},
|
|
|
|
heading5: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
marginTop: theme.spacing[3],
|
|
marginBottom: theme.spacing[1],
|
|
lineHeight: 22,
|
|
},
|
|
|
|
heading6: {
|
|
...webSelectableTextStyle,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foregroundMuted,
|
|
marginTop: theme.spacing[3],
|
|
marginBottom: theme.spacing[1],
|
|
lineHeight: 20,
|
|
textTransform: "uppercase" as const,
|
|
letterSpacing: 0.5,
|
|
},
|
|
|
|
// =========================================================================
|
|
// TEXT FORMATTING
|
|
// =========================================================================
|
|
|
|
strong: {
|
|
...webSelectableTextStyle,
|
|
fontWeight: theme.fontWeight.medium,
|
|
},
|
|
|
|
em: {
|
|
...webSelectableTextStyle,
|
|
fontStyle: "italic" as const,
|
|
},
|
|
|
|
s: {
|
|
...webSelectableTextStyle,
|
|
textDecorationLine: "line-through" as const,
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
|
|
link: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.accentBright,
|
|
textDecorationLine: "none" as const,
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
overflowWrap: "anywhere" as const,
|
|
},
|
|
|
|
blocklink: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.accentBright,
|
|
textDecorationLine: "none" as const,
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
overflowWrap: "anywhere" as const,
|
|
},
|
|
|
|
// =========================================================================
|
|
// CODE
|
|
// =========================================================================
|
|
|
|
code_inline: {
|
|
...webSelectableTextStyle,
|
|
backgroundColor: theme.colors.surface2,
|
|
color: theme.colors.foreground,
|
|
paddingHorizontal: theme.spacing[1],
|
|
paddingVertical: 2,
|
|
borderRadius: theme.borderRadius.md,
|
|
borderWidth: 0,
|
|
fontFamily: theme.fontFamily.mono,
|
|
fontSize: theme.fontSize.code,
|
|
},
|
|
|
|
code_block: {
|
|
...webSelectableTextStyle,
|
|
backgroundColor: theme.colors.surface2,
|
|
color: theme.colors.foreground,
|
|
padding: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.md,
|
|
fontFamily: theme.fontFamily.mono,
|
|
fontSize: theme.fontSize.code,
|
|
marginVertical: theme.spacing[2],
|
|
},
|
|
|
|
fence: {
|
|
...webSelectableTextStyle,
|
|
backgroundColor: theme.colors.surface2,
|
|
color: theme.colors.foreground,
|
|
padding: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
fontFamily: theme.fontFamily.mono,
|
|
fontSize: theme.fontSize.code,
|
|
marginVertical: theme.spacing[3],
|
|
},
|
|
|
|
pre: {
|
|
marginVertical: theme.spacing[2],
|
|
},
|
|
|
|
// =========================================================================
|
|
// TABLES
|
|
// =========================================================================
|
|
|
|
table: {
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.md,
|
|
marginVertical: theme.spacing[3],
|
|
},
|
|
|
|
thead: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
|
|
tbody: {},
|
|
|
|
th: {
|
|
...webSelectableTextStyle,
|
|
padding: theme.spacing[2],
|
|
borderBottomWidth: 1,
|
|
borderRightWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
backgroundColor: theme.colors.surface2,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
textAlign: "left" as const,
|
|
},
|
|
|
|
tr: {
|
|
borderBottomWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
flexDirection: "row" as const,
|
|
},
|
|
|
|
td: {
|
|
...webSelectableTextStyle,
|
|
padding: theme.spacing[2],
|
|
borderRightWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
flex: 1,
|
|
},
|
|
|
|
// =========================================================================
|
|
// LISTS
|
|
// =========================================================================
|
|
|
|
bullet_list: {
|
|
paddingLeft: 0,
|
|
width: "100%" as const,
|
|
},
|
|
|
|
ordered_list: {
|
|
paddingLeft: 0,
|
|
width: "100%" as const,
|
|
},
|
|
|
|
list_item: {
|
|
marginBottom: theme.spacing[1],
|
|
flexDirection: "row" as const,
|
|
alignItems: "flex-start" as const,
|
|
flexShrink: 1,
|
|
},
|
|
|
|
bullet_list_content: {
|
|
flex: 1,
|
|
flexShrink: 1,
|
|
},
|
|
|
|
ordered_list_content: {
|
|
flex: 1,
|
|
flexShrink: 1,
|
|
},
|
|
|
|
bullet_list_icon: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.foregroundMuted,
|
|
marginRight: 4,
|
|
fontSize: theme.fontSize.base,
|
|
lineHeight: 22,
|
|
},
|
|
|
|
ordered_list_icon: {
|
|
...webSelectableTextStyle,
|
|
color: theme.colors.foregroundMuted,
|
|
marginRight: 4,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.normal,
|
|
lineHeight: 22,
|
|
minWidth: 12,
|
|
},
|
|
|
|
// =========================================================================
|
|
// BLOCKQUOTE
|
|
// =========================================================================
|
|
|
|
blockquote: {
|
|
backgroundColor: theme.colors.surface2,
|
|
borderLeftWidth: 4,
|
|
borderLeftColor: theme.colors.primary,
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
marginVertical: theme.spacing[3],
|
|
borderRadius: theme.borderRadius.md,
|
|
},
|
|
|
|
// =========================================================================
|
|
// HORIZONTAL RULE
|
|
// =========================================================================
|
|
|
|
hr: {
|
|
backgroundColor: theme.colors.border,
|
|
height: 1,
|
|
marginVertical: theme.spacing[6],
|
|
},
|
|
|
|
// =========================================================================
|
|
// IMAGES
|
|
// =========================================================================
|
|
|
|
image: {
|
|
borderRadius: theme.borderRadius.md,
|
|
marginVertical: theme.spacing[2],
|
|
},
|
|
|
|
// =========================================================================
|
|
// BREAKS
|
|
// =========================================================================
|
|
|
|
hardbreak: {
|
|
height: theme.spacing[2],
|
|
},
|
|
|
|
softbreak: {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a smaller variant of markdown styles for compact UI elements
|
|
* like thought bubbles, tooltips, or side panels.
|
|
*/
|
|
export function createCompactMarkdownStyles(theme: Theme) {
|
|
const baseStyles = createMarkdownStyles(theme);
|
|
|
|
return {
|
|
...baseStyles,
|
|
|
|
body: {
|
|
...baseStyles.body,
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: 20,
|
|
},
|
|
|
|
heading1: {
|
|
...baseStyles.heading1,
|
|
fontSize: theme.fontSize.xl,
|
|
marginTop: theme.spacing[4],
|
|
marginBottom: theme.spacing[2],
|
|
lineHeight: 26,
|
|
},
|
|
|
|
heading2: {
|
|
...baseStyles.heading2,
|
|
fontSize: theme.fontSize.lg,
|
|
marginTop: theme.spacing[3],
|
|
marginBottom: theme.spacing[2],
|
|
lineHeight: 24,
|
|
},
|
|
|
|
heading3: {
|
|
...baseStyles.heading3,
|
|
fontSize: theme.fontSize.base,
|
|
marginTop: theme.spacing[3],
|
|
marginBottom: theme.spacing[1],
|
|
lineHeight: 22,
|
|
},
|
|
|
|
paragraph: {
|
|
...baseStyles.paragraph,
|
|
marginBottom: theme.spacing[2],
|
|
},
|
|
|
|
code_inline: {
|
|
...baseStyles.code_inline,
|
|
fontSize: theme.fontSize.sm - 3,
|
|
},
|
|
|
|
code_block: {
|
|
...baseStyles.code_block,
|
|
fontSize: theme.fontSize.code,
|
|
padding: theme.spacing[2],
|
|
},
|
|
|
|
fence: {
|
|
...baseStyles.fence,
|
|
fontSize: theme.fontSize.code,
|
|
padding: theme.spacing[2],
|
|
},
|
|
};
|
|
}
|