Skip to content

Commit 75e0c5b

Browse files
yyq1025claude
andcommitted
app: pierre theme + bordered editor-bg code containers + JetBrains Mono
- shiki themes swap github-light/dark → @pierre/theme pierre-light/dark (the package the Pierre diff webview already renders with — chat code and the tool-call diff sheet now share one vocabulary). toShikiTheme maps the VS Code shape (tokenColors) into shiki's type and drops object-form semanticTokenColors; the copy is also load-bearing because shiki's normalizeTheme mutates its input and the exports are frozen - code block containers sit on the theme's own editor background (#ffffff / #0a0a0a) with a 1pt border carrying the block boundary — the Pierre diff-sheet card look; applies to both the broken-out CodeBlockSegment and enriched-rendered nested blocks - JetBrains Mono via the expo-font config plugin (Regular + Bold TTFs sourced from @expo-google-fonts/jetbrains-mono — build-time embed, NOT useFonts: the transcript needs the font synchronously at first native render). Both enriched (RCTFont) and the TextInput container resolve the "JetBrains Mono" family; matches the diff webview's woff2 - resolveLang reads only the FIRST word of the fence info string — marked's .lang is the entire string ("ts title=x"), which missed the alias table and rendered plain Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 22a4fd8 commit 75e0c5b

5 files changed

Lines changed: 72 additions & 18 deletions

File tree

packages/app/app.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@
7474
],
7575
"expo-image",
7676
"expo-web-browser",
77-
"expo-font",
77+
[
78+
"expo-font",
79+
{
80+
"fonts": [
81+
"../../node_modules/@expo-google-fonts/jetbrains-mono/400Regular/JetBrainsMono_400Regular.ttf",
82+
"../../node_modules/@expo-google-fonts/jetbrains-mono/700Bold/JetBrainsMono_700Bold.ttf"
83+
]
84+
}
85+
],
7886
"@config-plugins/react-native-webrtc"
7987
],
8088
"experiments": {

packages/app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
},
1616
"dependencies": {
1717
"@config-plugins/react-native-webrtc": "^15.0.1",
18+
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
1819
"@expo/metro-runtime": "~56.0.15",
1920
"@expo/ui": "~56.0.17",
2021
"@legendapp/list": "3.0.4",
2122
"@nandorojo/galeria": "^3.0.3",
2223
"@noble/ed25519": "^3.1.0",
2324
"@noble/hashes": "^2.2.0",
2425
"@pierre/diffs": "^1.2.7",
26+
"@pierre/theme": "^1.0.3",
2527
"@shikijs/core": "^4.2.0",
2628
"@shikijs/langs": "^4.2.0",
27-
"@shikijs/themes": "^4.2.0",
2829
"@sidecodeapp/protocol": "workspace:*",
2930
"@swmansion/react-native-bottom-sheet": "0.14.1",
3031
"@tanstack/react-db": "^0.1.86",

packages/app/src/lib/markdown/chat-markdown.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ const BODY_LINE_HEIGHT = 22;
4949
// (still enriched, inside runs) and block code (ours) read as one family.
5050
export const CODE_FONT_SIZE = 14;
5151
export const CODE_LINE_HEIGHT = 19;
52+
// JetBrains Mono, embedded at build time via the expo-font config plugin
53+
// (app.json; Regular + Bold TTFs from @expo-google-fonts/jetbrains-mono —
54+
// the package is only the TTF source, we deliberately do NOT use its
55+
// useFonts runtime loading: the transcript needs the font synchronously
56+
// at first native render, no flash-of-fallback). Family-name resolution:
57+
// both enriched (RCTFont via StyleConfig) and RN TextInput resolve
58+
// "JetBrains Mono" + weight against the embedded family. Matches the
59+
// Pierre diff webview, which already ships JetBrains Mono woff2.
60+
export const CODE_FONT_FAMILY = "JetBrains Mono";
5261

5362
interface ColorPalette {
5463
text: string;
@@ -71,8 +80,11 @@ export const LIGHT_PALETTE: ColorPalette = {
7180
link: "#2563eb",
7281
inlineCodeBg: "#f4f4f5",
7382
inlineCodeBorder: "#f4f4f5",
74-
codeBlockBg: "#f4f4f5",
75-
codeBlockBorder: "#f4f4f5",
83+
// Code blocks sit on the Pierre theme's own editor background (the
84+
// same surface the Pierre diff sheet renders on) with a visible border
85+
// carrying the block boundary — the bg matches the chat bg in both modes.
86+
codeBlockBg: "#ffffff",
87+
codeBlockBorder: "#e4e4e7",
7688
blockquoteBorder: "#d4d4d8",
7789
bullet: "#525252",
7890
tableHeaderBg: "#eeeeef",
@@ -86,8 +98,9 @@ export const DARK_PALETTE: ColorPalette = {
8698
link: "#60a5fa",
8799
inlineCodeBg: "#27272a",
88100
inlineCodeBorder: "#27272a",
89-
codeBlockBg: "#18181b",
90-
codeBlockBorder: "#18181b",
101+
// pierre-dark editor.background; border = zinc-800 (see light note).
102+
codeBlockBg: "#0a0a0a",
103+
codeBlockBorder: "#27272a",
91104
blockquoteBorder: "#3f3f46",
92105
bullet: "#a1a1aa",
93106
tableHeaderBg: "#27272a", // zinc-800 — one shade lighter than chat bg
@@ -154,19 +167,20 @@ function buildStyle(p: ColorPalette): MarkdownStyle {
154167
marginBottom: 12,
155168
},
156169
code: {
157-
fontFamily: "Menlo",
170+
fontFamily: CODE_FONT_FAMILY,
158171
fontSize: CODE_FONT_SIZE,
159172
color: p.text,
160173
backgroundColor: p.inlineCodeBg,
161174
borderColor: p.inlineCodeBorder,
162175
},
163176
codeBlock: {
164-
fontFamily: "Menlo",
177+
fontFamily: CODE_FONT_FAMILY,
165178
fontSize: CODE_FONT_SIZE,
166179
lineHeight: CODE_LINE_HEIGHT,
167180
color: p.text,
168181
backgroundColor: p.codeBlockBg,
169182
borderColor: p.codeBlockBorder,
183+
borderWidth: 1,
170184
borderRadius: 8,
171185
padding: 10,
172186
marginTop: 0,

packages/app/src/lib/markdown/chunked-markdown.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import {
1111
ChatMarkdown,
12+
CODE_FONT_FAMILY,
1213
CODE_FONT_SIZE,
1314
CODE_LINE_HEIGHT,
1415
DARK_PALETTE,
@@ -26,7 +27,7 @@ import { useRemend } from "./remend";
2627
* - RUNS (text + tables) still render through enriched via ChatMarkdown
2728
* — native attributed-text selection stays continuous across
2829
* paragraphs and tables, and typography is byte-identical to before.
29-
* - CODE BLOCKS break out into a horizontal-scroll Menlo code view with
30+
* - CODE BLOCKS break out into a horizontal-scroll JetBrains Mono view with
3031
* react-native-shiki-engine STREAMING highlight: naive full
3132
* re-tokenize per delta (~1-3.5ms measured on device for typical
3233
* blocks) + per-line memo keyed on token content, so settled lines
@@ -38,7 +39,7 @@ import { useRemend } from "./remend";
3839
* UITextView, which gives PARTIAL selection with handles (RN iOS <Text
3940
* selectable> is select-all only, facebook/react-native#13938). All
4041
* highlight spans nest inside it as one attributed string. Spans are
41-
* color-only over the same Menlo metrics as the plain fallback → zero
42+
* color-only over the same monospace metrics as the plain fallback → zero
4243
* layout shift when the highlighter comes online.
4344
*
4445
* `streamDone` gates tail-run remend repair and EOF fence-close marking.
@@ -131,6 +132,8 @@ const CodeBlockSegment = memo(function CodeBlockSegment({
131132
className="rounded-lg overflow-hidden"
132133
style={{
133134
backgroundColor: palette.codeBlockBg,
135+
borderWidth: 1,
136+
borderColor: palette.codeBlockBorder,
134137
}}
135138
>
136139
<ScrollView horizontal>
@@ -143,7 +146,7 @@ const CodeBlockSegment = memo(function CodeBlockSegment({
143146
// competes with the transcript list / horizontal ScrollView.
144147
scrollEnabled={false}
145148
style={{
146-
fontFamily: "Menlo",
149+
fontFamily: CODE_FONT_FAMILY,
147150
fontSize: CODE_FONT_SIZE,
148151
lineHeight: CODE_LINE_HEIGHT,
149152
color: palette.text,

packages/app/src/lib/markdown/code-highlighter.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { HighlighterCore, ThemedToken } from "@shikijs/core";
1+
import themePierreDark from "@pierre/theme/pierre-dark";
2+
import themePierreLight from "@pierre/theme/pierre-light";
3+
import type {
4+
HighlighterCore,
5+
ThemedToken,
6+
ThemeRegistration,
7+
} from "@shikijs/core";
28
import { createHighlighterCore } from "@shikijs/core";
39
import langDiff from "@shikijs/langs/diff";
410
import langJson from "@shikijs/langs/json";
@@ -13,8 +19,6 @@ import langSwift from "@shikijs/langs/swift";
1319
import langToml from "@shikijs/langs/toml";
1420
import langTsx from "@shikijs/langs/tsx";
1521
import langYaml from "@shikijs/langs/yaml";
16-
import themeGithubDark from "@shikijs/themes/github-dark";
17-
import themeGithubLight from "@shikijs/themes/github-light";
1822
import { useMemo, useSyncExternalStore } from "react";
1923
import {
2024
createNativeEngine,
@@ -43,6 +47,21 @@ let highlighter: HighlighterCore | null = null;
4347
let initStarted = false;
4448
const listeners = new Set<() => void>();
4549

50+
/** @pierre/theme ships VS Code-shaped themes (`tokenColors`,
51+
* object-form `semanticTokenColors`); shiki's normalizeTheme handles
52+
* tokenColors at runtime (moves them → settings) but its ThemeInput TYPE
53+
* doesn't admit them — map explicitly. semanticTokenColors is dropped: a
54+
* VS Code/LSP semantic-highlighting feature shiki's textmate tokenizer
55+
* never reads. The copy is also load-bearing: normalizeTheme MUTATES its
56+
* input and the package exports are Object.freeze'd. */
57+
function toShikiTheme(
58+
theme: typeof themePierreDark,
59+
name: string,
60+
): ThemeRegistration {
61+
const { tokenColors, semanticTokenColors: _drop, ...rest } = theme;
62+
return { ...rest, name, settings: [...tokenColors] };
63+
}
64+
4665
/** Kick off engine + grammar loading. Idempotent. Called from the root
4766
* layout once the launch settles (the engine README recommends app-start
4867
* init; the grammar JSON.parse burst (~535KB source) belongs in the
@@ -58,7 +77,14 @@ export function initHighlighter() {
5877
return;
5978
}
6079
createHighlighterCore({
61-
themes: [themeGithubLight, themeGithubDark],
80+
// Pierre theme (same package the Pierre diff webview renders with —
81+
// chat code blocks and the tool-call diff sheet share one vocabulary),
82+
// registered under the kebab ids pierre-view already uses (the exports
83+
// carry display names, "Pierre Dark").
84+
themes: [
85+
toShikiTheme(themePierreLight, "pierre-light"),
86+
toShikiTheme(themePierreDark, "pierre-dark"),
87+
],
6288
langs: [
6389
langTsx,
6490
langSwift,
@@ -149,9 +175,11 @@ const PLAIN_LANGS = new Set([
149175

150176
const loggedMisses = new Set<string>();
151177

152-
/** Markdown info-string → loaded grammar id (null = render plain). */
178+
/** Markdown info-string → loaded grammar id (null = render plain).
179+
* Only the FIRST word counts: marked's `.lang` is the entire info string
180+
* ("ts title=foo.ts"), and GFM defines the language as its first word. */
153181
export function resolveLang(infoString: string): string | null {
154-
const norm = infoString.trim().toLowerCase();
182+
const norm = infoString.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "";
155183
if (PLAIN_LANGS.has(norm)) return null;
156184
const id = LANG_ALIASES[norm] ?? null;
157185
if (id === null && __DEV__ && !loggedMisses.has(norm)) {
@@ -173,7 +201,7 @@ export function tokenizeCode(
173201
): TokenLines {
174202
return hl.codeToTokensBase(code, {
175203
lang,
176-
theme: scheme === "dark" ? "github-dark" : "github-light",
204+
theme: scheme === "dark" ? "pierre-dark" : "pierre-light",
177205
});
178206
}
179207

0 commit comments

Comments
 (0)