1import { Platform, ScrollView, type StyleProp, type TextStyle, View } from "react-native";
2
3import { AppText as Text } from "../../../../components/AppText";
4import {
5 resolveMarkdownFontSizes,
6 resolveMobileCodeSurface,
7} from "../../../../lib/appearancePreferences";
8import { getMobileTerminalTheme } from "../../../terminal/terminalTheme";
9import { useAppearancePreferences } from "../AppearancePreferencesProvider";
10
11const CODE_FONT_FAMILY = Platform.select({
12 ios: "ui-monospace",
13 android: "monospace",
14 default: "monospace",
15});
16
17/** Hairline between a section's preview surface and its control rows. */
18export function AppearancePreviewSeparator() {
19 return <View className="h-px bg-separator" />;
20}
21
22/** Live sample of body text rendered at the chosen base font size. */
23export function TextAppearancePreview(props: { readonly fontSize: number }) {
24 const sizes = resolveMarkdownFontSizes(props.fontSize);
25
26 return (
27 <View className="gap-1 p-4">
28 <Text
29 className="text-foreground"
30 style={{ fontSize: sizes.m, lineHeight: sizes.bodyLineHeight }}
31 >
32 The quick brown fox jumps over the lazy dog.
33 </Text>
34 <Text
35 className="text-foreground-muted"
36 style={{ fontSize: sizes.s, lineHeight: Math.round(sizes.s * 1.4) }}
37 >
38 Messages, labels, and headings scale with this size.
39 </Text>
40 </View>
41 );
42}
43
44/**
45 * Live terminal sample using the real terminal theme's text colors and font,
46 * on the shared card background so it reads like the other previews.
47 */
48export function TerminalAppearancePreview(props: { readonly fontSize: number }) {
49 const { themeAppearance: scheme, themeId } = useAppearancePreferences();
50 const theme = getMobileTerminalTheme(themeId, scheme);
51 const lineHeight = Math.round(props.fontSize * 1.6);
52 const lineStyle = {
53 fontFamily: "Menlo",
54 fontSize: props.fontSize,
55 lineHeight,
56 } as const;
57 // AppText stamps the sans font on every node, so nested spans must
58 // re-apply the terminal font instead of relying on inheritance, exactly
59 // like the code preview's tokens below.
60 const span = (color: string, extra?: TextStyle): StyleProp<TextStyle> => [
61 lineStyle,
62 { color, ...extra },
63 ];
64
65 return (
66 <View className="p-4">
67 <Text style={span(theme.foreground)}>
68 <Text style={span(theme.palette[2])}>→ </Text>
69 <Text style={span(theme.palette[6])}>t3code </Text>
70 <Text style={span(theme.palette[4])}>git:(</Text>
71 <Text style={span(theme.palette[1])}>main</Text>
72 <Text style={span(theme.palette[4])}>)</Text>
73 <Text style={span(theme.palette[3])}> ✗</Text>
74 <Text style={span(theme.foreground)}> vpr dev</Text>
75 </Text>
76 <Text style={span(theme.foreground)}>
77 <Text style={span(theme.palette[2])}>VITE v7.1.1</Text>
78 <Text style={span(theme.mutedForeground)}> ready in</Text>
79 <Text style={span(theme.foreground)}> 1.24s</Text>
80 </Text>
81 <Text style={span(theme.foreground)}>
82 <Text style={span(theme.palette[2])}>→ </Text>
83 <Text style={span(theme.mutedForeground)}>Local: </Text>
84 <Text style={span(theme.palette[6], { textDecorationLine: "underline" })}>
85 http://127.0.0.1:5173/
86 </Text>
87 </Text>
88 <Text style={span(theme.foreground)}>
89 <Text style={span(theme.palette[2])}>✓ 85 passed</Text>
90 <Text style={span(theme.palette[3])}> △ 2 warnings</Text>
91 <Text style={span(theme.palette[1])}> ✗ 0 failed</Text>
92 </Text>
93 <Text style={span(theme.foreground)}>
94 <Text style={span(theme.background, { backgroundColor: theme.palette[2] })}>
95 {" READY "}
96 </Text>
97 <Text style={span(theme.mutedForeground)}> watching for changes</Text>{" "}
98 <Text style={span(theme.cursorForeground)}>▏</Text>
99 </Text>
100 </View>
101 );
102}
103
104interface CodePreviewToken {
105 readonly text: string;
106 readonly keyword?: boolean;
107}
108
109interface CodePreviewLine {
110 readonly id: string;
111 readonly tokens: ReadonlyArray<CodePreviewToken>;
112}
113
114const CODE_PREVIEW_LINES: ReadonlyArray<CodePreviewLine> = [
115 {
116 id: "signature",
117 tokens: [{ text: "function", keyword: true }, { text: " formatUser(user) {" }],
118 },
119 {
120 id: "body",
121 tokens: [
122 { text: " " },
123 { text: "return", keyword: true },
124 { text: " `${user.name} <${user.email}>` // demonstrates how long lines behave" },
125 ],
126 },
127 { id: "close", tokens: [{ text: "}" }] },
128];
129
130/**
131 * Live code sample matching the code & diff surface metrics. Long lines wrap
132 * when word break is on and scroll horizontally when it is off, mirroring the
133 * real code surface.
134 */
135export function CodeAppearancePreview(props: {
136 readonly fontSize: number;
137 readonly wordBreak: boolean;
138}) {
139 const surface = resolveMobileCodeSurface(props.fontSize);
140
141 const lineNumber = (line: CodePreviewLine, index: number) => (
142 <Text
143 className="text-right text-icon-subtle"
144 key={line.id}
145 style={{
146 fontFamily: CODE_FONT_FAMILY,
147 fontSize: surface.lineNumberFontSize,
148 lineHeight: surface.rowHeight,
149 width: 22,
150 }}
151 >
152 {index + 1}
153 </Text>
154 );
155
156 const codeLine = (line: CodePreviewLine, wrap: boolean) => (
157 <Text
158 className="text-foreground"
159 key={line.id}
160 numberOfLines={wrap ? undefined : 1}
161 style={{
162 fontFamily: CODE_FONT_FAMILY,
163 fontSize: surface.fontSize,
164 lineHeight: surface.rowHeight,
165 }}
166 >
167 {line.tokens.map((token) => (
168 <Text
169 key={token.text}
170 className={token.keyword ? "text-md-link" : undefined}
171 style={{
172 fontFamily: CODE_FONT_FAMILY,
173 fontSize: surface.fontSize,
174 lineHeight: surface.rowHeight,
175 }}
176 >
177 {token.text}
178 </Text>
179 ))}
180 </Text>
181 );
182
183 if (props.wordBreak) {
184 return (
185 <View className="p-4">
186 {CODE_PREVIEW_LINES.map((line, index) => (
187 <View className="flex-row" key={line.id}>
188 {lineNumber(line, index)}
189 <View className="flex-1 pl-3">{codeLine(line, true)}</View>
190 </View>
191 ))}
192 </View>
193 );
194 }
195
196 return (
197 <View className="flex-row p-4">
198 <View>{CODE_PREVIEW_LINES.map((line, index) => lineNumber(line, index))}</View>
199 <ScrollView
200 horizontal
201 contentContainerStyle={{ paddingLeft: 12 }}
202 showsHorizontalScrollIndicator={false}
203 >
204 <View>{CODE_PREVIEW_LINES.map((line) => codeLine(line, false))}</View>
205 </ScrollView>
206 </View>
207 );
208}