Skip to content

Commit 57dedf1

Browse files
committed
feat(web): customize diff colors and markers
1 parent a4cc136 commit 57dedf1

11 files changed

Lines changed: 392 additions & 23 deletions

File tree

apps/web/src/components/chat/MessagesTimeline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
type ReactNode,
3131
} from "react";
3232
import { LegendList, type LegendListRef } from "@legendapp/list/react";
33-
import { FileDiff } from "@pierre/diffs/react";
3433
import {
3534
deriveTimelineEntries,
3635
workEntryIndicatesToolFailure,
@@ -45,6 +44,7 @@ import {
4544
resolveFileDiffPath,
4645
} from "../../lib/diffRendering";
4746
import ChatMarkdown from "../ChatMarkdown";
47+
import { StyledFileDiff } from "../diffs/StyledDiffCodeView";
4848
import {
4949
BotIcon,
5050
CheckIcon,
@@ -1869,7 +1869,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
18691869
)}
18701870
{renderablePatch?.kind === "files" &&
18711871
renderablePatch.files.map((fileDiff) => (
1872-
<FileDiff
1872+
<StyledFileDiff
18731873
key={resolveFileDiffPath(fileDiff)}
18741874
fileDiff={fileDiff}
18751875
options={{

apps/web/src/components/diffs/StyledDiffCodeView.test.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
44
const testState = vi.hoisted(() => ({
55
codeViewClassName: null as string | null,
66
codeViewOptions: null as Record<string, unknown> | null,
7+
fileDiffClassName: null as string | null,
8+
fileDiffOptions: null as Record<string, unknown> | null,
79
}));
810

911
vi.mock("@pierre/diffs/react", () => ({
@@ -12,14 +14,25 @@ vi.mock("@pierre/diffs/react", () => ({
1214
testState.codeViewOptions = props.options;
1315
return null;
1416
},
17+
FileDiff: (props: { className: string; options: Record<string, unknown> }) => {
18+
testState.fileDiffClassName = props.className;
19+
testState.fileDiffOptions = props.options;
20+
return null;
21+
},
1522
}));
1623

17-
import { StyledDiffCodeView } from "./StyledDiffCodeView";
24+
import {
25+
getDiffColorSchemeClassName,
26+
StyledDiffCodeView,
27+
StyledFileDiff,
28+
} from "./StyledDiffCodeView";
1829

1930
describe("StyledDiffCodeView", () => {
2031
beforeEach(() => {
2132
testState.codeViewClassName = null;
2233
testState.codeViewOptions = null;
34+
testState.fileDiffClassName = null;
35+
testState.fileDiffOptions = null;
2336
});
2437

2538
it("always pairs the shared diff styling with its virtualized geometry", () => {
@@ -36,12 +49,15 @@ describe("StyledDiffCodeView", () => {
3649
);
3750

3851
expect(testState.codeViewClassName).toBe(
39-
"diff-render-surface [--code-background:var(--background)] outline-none min-h-0",
52+
"diff-render-surface [--code-background:var(--background)] outline-none " +
53+
"[--t3-diff-addition-color:var(--success)] " +
54+
"[--t3-diff-deletion-color:var(--destructive)] min-h-0",
4055
);
4156
expect(testState.codeViewOptions).toMatchObject({
4257
theme: "pierre-dark",
4358
stickyHeaders: true,
4459
loadDiffFiles,
60+
diffIndicators: "bars",
4561
itemMetrics: {
4662
diffHeaderHeight: 32,
4763
hunkSeparatorHeight: 24,
@@ -57,4 +73,38 @@ describe("StyledDiffCodeView", () => {
5773
expect.stringContaining(")[data-expand-index]\n [data-unmodified-lines]"),
5874
);
5975
});
76+
77+
it("maps the alternate palette to blue additions and orange deletions", () => {
78+
expect(getDiffColorSchemeClassName("orange-blue")).toBe(
79+
"[--t3-diff-addition-color:var(--info)] [--t3-diff-deletion-color:var(--warning)]",
80+
);
81+
});
82+
83+
it("applies the same appearance defaults to compact file diffs", () => {
84+
renderToStaticMarkup(
85+
<StyledFileDiff
86+
fileDiff={{
87+
name: "app.ts",
88+
type: "change",
89+
hunks: [],
90+
splitLineCount: 0,
91+
unifiedLineCount: 0,
92+
isPartial: true,
93+
deletionLines: [],
94+
additionLines: [],
95+
}}
96+
options={{ diffStyle: "unified", theme: "pierre-dark" }}
97+
/>,
98+
);
99+
100+
expect(testState.fileDiffClassName).toContain("[--t3-diff-addition-color:var(--success)]");
101+
expect(testState.fileDiffOptions).toMatchObject({
102+
diffStyle: "unified",
103+
theme: "pierre-dark",
104+
diffIndicators: "bars",
105+
});
106+
expect(testState.fileDiffOptions?.unsafeCSS).toEqual(
107+
expect.stringContaining("--diffs-addition-color-override"),
108+
);
109+
});
60110
});

apps/web/src/components/diffs/StyledDiffCodeView.tsx

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import {
44
type CodeViewHandle,
55
type CodeViewProps,
66
type ControlledCodeViewProps,
7+
FileDiff,
8+
type FileDiffProps,
79
type UncontrolledCodeViewProps,
810
} from "@pierre/diffs/react";
911
/* oxlint-enable eslint/no-restricted-imports */
1012
import type { Ref } from "react";
13+
import type { DiffColorScheme } from "@t3tools/contracts/settings";
1114

15+
import { useClientSettings } from "~/hooks/useSettings";
1216
import { DIFF_SURFACE_THEME_UNSAFE_CSS } from "~/lib/diffRendering";
1317

1418
const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS}
@@ -258,9 +262,29 @@ const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS}
258262
}
259263
`;
260264

265+
const DIFF_COLOR_SCHEME_CLASSES: Record<DiffColorScheme, string> = {
266+
"red-green":
267+
"[--t3-diff-addition-color:var(--success)] [--t3-diff-deletion-color:var(--destructive)]",
268+
"orange-blue": "[--t3-diff-addition-color:var(--info)] [--t3-diff-deletion-color:var(--warning)]",
269+
};
270+
271+
export function getDiffColorSchemeClassName(scheme: DiffColorScheme): string {
272+
return DIFF_COLOR_SCHEME_CLASSES[scheme];
273+
}
274+
275+
function getDiffSurfaceClassName(colorScheme: DiffColorScheme, className?: string): string {
276+
return [
277+
"diff-render-surface [--code-background:var(--background)] outline-none",
278+
getDiffColorSchemeClassName(colorScheme),
279+
className,
280+
]
281+
.filter(Boolean)
282+
.join(" ");
283+
}
284+
261285
export type StyledDiffCodeViewOptions<LAnnotation> = Omit<
262286
NonNullable<CodeViewProps<LAnnotation>["options"]>,
263-
"unsafeCSS" | "itemMetrics" | "layout"
287+
"unsafeCSS" | "itemMetrics" | "layout" | "diffIndicators"
264288
>;
265289

266290
type StyledDiffCodeViewProps<LAnnotation> = (
@@ -284,19 +308,19 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
284308
unsafeCSSExtra,
285309
...props
286310
}: StyledDiffCodeViewProps<LAnnotation>) {
311+
const diffColorScheme = useClientSettings((settings) => settings.diffColorScheme);
312+
const diffIndicatorStyle = useClientSettings((settings) => settings.diffIndicatorStyle);
313+
287314
return (
288315
<CodeView<LAnnotation>
289316
{...props}
290317
{...(viewerRef ? { ref: viewerRef } : {})}
291318
// The custom element itself is focusable for keyboard scrolling. Its native outline sits
292319
// outside the panel clipping boundary; actual controls inside retain their own indicators.
293-
className={
294-
className
295-
? `diff-render-surface [--code-background:var(--background)] outline-none ${className}`
296-
: "diff-render-surface [--code-background:var(--background)] outline-none"
297-
}
320+
className={getDiffSurfaceClassName(diffColorScheme, className)}
298321
options={{
299322
...options,
323+
diffIndicators: diffIndicatorStyle,
300324
unsafeCSS: unsafeCSSExtra
301325
? `${DIFF_VIEW_UNSAFE_CSS}\n${unsafeCSSExtra}`
302326
: DIFF_VIEW_UNSAFE_CSS,
@@ -320,3 +344,34 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
320344
/>
321345
);
322346
}
347+
348+
export type StyledFileDiffOptions<LAnnotation> = Omit<
349+
NonNullable<FileDiffProps<LAnnotation>["options"]>,
350+
"unsafeCSS" | "diffIndicators"
351+
>;
352+
353+
type StyledFileDiffProps<LAnnotation> = Omit<FileDiffProps<LAnnotation>, "options"> & {
354+
readonly options?: StyledFileDiffOptions<LAnnotation>;
355+
};
356+
357+
/** The non-virtualized counterpart used for compact inline review-comment diffs. */
358+
export function StyledFileDiff<LAnnotation = undefined>({
359+
options,
360+
className,
361+
...props
362+
}: StyledFileDiffProps<LAnnotation>) {
363+
const diffColorScheme = useClientSettings((settings) => settings.diffColorScheme);
364+
const diffIndicatorStyle = useClientSettings((settings) => settings.diffIndicatorStyle);
365+
366+
return (
367+
<FileDiff<LAnnotation>
368+
{...props}
369+
className={getDiffSurfaceClassName(diffColorScheme, className)}
370+
options={{
371+
...options,
372+
diffIndicators: diffIndicatorStyle,
373+
unsafeCSS: DIFF_SURFACE_THEME_UNSAFE_CSS,
374+
}}
375+
/>
376+
);
377+
}

0 commit comments

Comments
 (0)