@reviewflow/ui 6.24.0 → 6.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +17 -2
  2. package/src/components/branch-diff/diff-font.ts +26 -0
  3. package/src/components/branch-diff/diff-themes.ts +53 -1
  4. package/src/components/branch-diff/reviewflow-diff-selection.ts +11 -0
  5. package/src/components/branch-diff/reviewflow-diff-themes.ts +2218 -0
  6. package/src/components/branch-diff/use-diff-settings.tsx +15 -3
  7. package/src/components/branch-diff/whitespace-button.tsx +2 -2
  8. package/src/components/diff-settings-section.tsx +189 -0
  9. package/src/components/diff-theme-preview.tsx +63 -0
  10. package/src/components/file-kind.ts +91 -0
  11. package/src/components/file-type-icon.tsx +70 -0
  12. package/src/components/pull-request-diff-annotations.tsx +5 -2
  13. package/src/components/pull-request-diff-jump.ts +59 -1
  14. package/src/components/pull-request-diff-overview-tree.ts +57 -0
  15. package/src/components/pull-request-diff-overview.tsx +488 -88
  16. package/src/components/pull-request-diff-tree-files.ts +64 -0
  17. package/src/components/pull-request-diff-tree.tsx +232 -0
  18. package/src/components/pull-request-diff-view.tsx +111 -27
  19. package/src/components/pull-request-diff-viewer.tsx +23 -2
  20. package/src/components/pull-request-files.tsx +4 -4
  21. package/src/components/pull-request-panel.tsx +12 -6
  22. package/src/components/pull-request-screen.tsx +16 -4
  23. package/src/components/scroll-parent.ts +20 -0
  24. package/src/components/tooltip-icon-button.tsx +27 -0
  25. package/src/editor/code-editor.tsx +1 -0
  26. package/src/editor/code-token-colors.css +39 -0
  27. package/src/editor/codeHighlight.ts +2 -1
  28. package/src/editor/markdown-editor.css +1 -34
  29. package/src/editor/markdown-editor.tsx +1 -0
  30. package/src/index.ts +6 -6
  31. package/src/palette.ts +22 -0
  32. package/src/prStatus.ts +8 -1
  33. package/src/slots.tsx +8 -0
@@ -0,0 +1,64 @@
1
+ import type { PullRequestDiffResponse } from "@reviewflow/sdk";
2
+
3
+ export type DiffTreeFileStatus = "added" | "deleted" | "modified" | "renamed";
4
+
5
+ export interface DiffTreeFile {
6
+ path: string;
7
+ /** null for an omitted file, whose header the patch does not carry */
8
+ status: DiffTreeFileStatus | null;
9
+ }
10
+
11
+ const fileHeader = /^diff --git /mu;
12
+ const samePathHeader = /^a\/(.+) b\/\1$/u;
13
+ const renameTo = /^rename to (.+)$/mu;
14
+ const newFile = /^new file mode /mu;
15
+ const deletedFile = /^deleted file mode /mu;
16
+
17
+ const statusOfBlock = (block: string): DiffTreeFileStatus => {
18
+ if (newFile.test(block)) return "added";
19
+ if (deletedFile.test(block)) return "deleted";
20
+ if (renameTo.test(block)) return "renamed";
21
+ return "modified";
22
+ };
23
+
24
+ const pathOfBlock = (block: string): string | undefined => {
25
+ const renamed = renameTo.exec(block)?.[1];
26
+ if (renamed !== undefined) return renamed;
27
+ const firstLine = block.slice(0, block.indexOf("\n"));
28
+ return samePathHeader.exec(firstLine)?.[1];
29
+ };
30
+
31
+ /** Read from the patch's git headers, keyed by the path after any rename. */
32
+ export const fileStatusesOf = (
33
+ patch: string,
34
+ ): ReadonlyMap<string, DiffTreeFileStatus> =>
35
+ new Map(
36
+ patch
37
+ .split(fileHeader)
38
+ .slice(1)
39
+ .flatMap((block) => {
40
+ const path = pathOfBlock(block);
41
+ return path === undefined
42
+ ? []
43
+ : [[path, statusOfBlock(block)] as const];
44
+ }),
45
+ );
46
+
47
+ /**
48
+ * Every file of the range: the patch's, with their status, then the omitted
49
+ * ones. A patch file whose header cannot be read counts as modified.
50
+ */
51
+ export const diffTreeFilesOf = (
52
+ diff: Pick<PullRequestDiffResponse, "groups" | "omitted" | "patch">,
53
+ ): DiffTreeFile[] => {
54
+ const statuses = fileStatusesOf(diff.patch);
55
+ return [
56
+ ...diff.groups.flatMap(({ paths }) =>
57
+ paths.map((path) => ({
58
+ path,
59
+ status: statuses.get(path) ?? "modified",
60
+ })),
61
+ ),
62
+ ...diff.omitted.map(({ path }) => ({ path, status: null })),
63
+ ];
64
+ };
@@ -0,0 +1,232 @@
1
+ import { getResolvedOrResolveTheme } from "@pierre/diffs";
2
+ import type {
3
+ FileTree as FileTreeModel,
4
+ FileTreeRowDecoration,
5
+ GitStatusEntry,
6
+ } from "@pierre/trees";
7
+ import { themeToTreeStyles } from "@pierre/trees";
8
+ import { FileTree, useFileTree } from "@pierre/trees/react";
9
+ import type { AccentOrNeutral } from "alouette";
10
+ import { AccentScope, useCurrentMode } from "alouette";
11
+ import type { CSSProperties, ReactNode } from "react";
12
+ import { useEffect, useRef, useState } from "react";
13
+ import type { DiffTheme } from "./branch-diff/diff-themes.ts";
14
+ import { diffThemeNames } from "./branch-diff/diff-themes.ts";
15
+ import type { DiffTreeFile } from "./pull-request-diff-tree-files.ts";
16
+
17
+ export interface PullRequestDiffTreeProps {
18
+ files: DiffTreeFile[];
19
+ viewedPaths: readonly string[];
20
+ /** the diff's highlighting theme, whose git colors the statuses take */
21
+ theme: DiffTheme;
22
+ /** brings the file into view in the diff */
23
+ onFilePress: (path: string) => void;
24
+ }
25
+
26
+ interface DecorationData {
27
+ viewedPaths: ReadonlySet<string>;
28
+ omittedPaths: ReadonlySet<string>;
29
+ }
30
+
31
+ type CustomProperties = CSSProperties & Record<`--${string}`, string>;
32
+
33
+ /**
34
+ * Custom properties cross the shadow root, and a `var()` in one resolves where
35
+ * it is declared, inside alouette's theme: the tree takes the screen's tokens.
36
+ * A status takes the diff theme's git color, as the diff's file headers do, and
37
+ * the color of its accent when the theme has none (`CapturedAccent`).
38
+ */
39
+ const hostStyle = (
40
+ mode: "dark" | "light",
41
+ gitColors: CustomProperties,
42
+ ): CustomProperties => ({
43
+ ...gitColors,
44
+ height: "100%",
45
+ colorScheme: mode,
46
+ "--trees-bg-override": "transparent",
47
+ "--trees-fg-override": "var(--color-sharp)",
48
+ "--trees-fg-muted-override": "var(--color-muted)",
49
+ "--trees-bg-muted-override": "var(--color-interactive-soft-hover)",
50
+ "--trees-selected-bg-override": "var(--color-interactive-soft-active)",
51
+ "--trees-selected-fg-override": "var(--color-sharp)",
52
+ "--trees-border-color-override": "var(--color-border-muted)",
53
+ "--trees-indent-guide-bg-override": "var(--color-border-muted)",
54
+ "--trees-input-bg-override": "var(--color-lowered)",
55
+ "--trees-search-bg-override": "var(--color-lowered)",
56
+ "--trees-search-fg-override": "var(--color-sharp)",
57
+ "--trees-accent-override": "var(--tree-brand)",
58
+ "--trees-focus-ring-color-override": "var(--tree-brand)",
59
+ "--trees-status-added-override":
60
+ "var(--trees-theme-git-added-fg, var(--tree-success))",
61
+ "--trees-status-untracked-override":
62
+ "var(--trees-theme-git-untracked-fg, var(--tree-success))",
63
+ "--trees-status-modified-override":
64
+ "var(--trees-theme-git-modified-fg, var(--tree-info))",
65
+ "--trees-status-renamed-override":
66
+ "var(--trees-theme-git-renamed-fg, var(--tree-warning))",
67
+ "--trees-status-deleted-override":
68
+ "var(--trees-theme-git-deleted-fg, var(--tree-danger))",
69
+ "--trees-font-family-override": "var(--font-body)",
70
+ "--trees-font-size-override": "var(--text-sm, 13px)",
71
+ "--trees-padding-inline-override": "0px",
72
+ });
73
+
74
+ /** Holds `accent`'s color as `--tree-<accent>` for the subtree, whatever accent it is under. */
75
+ function CapturedAccent({
76
+ accent,
77
+ children,
78
+ }: {
79
+ accent: Exclude<AccentOrNeutral, "neutral">;
80
+ children: ReactNode;
81
+ }): ReactNode {
82
+ const style: CustomProperties = {
83
+ display: "contents",
84
+ [`--tree-${accent}`]: "var(--color-accent)",
85
+ };
86
+ return (
87
+ <AccentScope accent={accent}>
88
+ <div style={style}>{children}</div>
89
+ </AccentScope>
90
+ );
91
+ }
92
+
93
+ const gitColorsOf = (styles: Record<string, string>): CustomProperties =>
94
+ Object.fromEntries(
95
+ Object.entries(styles).filter(([key]) =>
96
+ key.startsWith("--trees-theme-git-"),
97
+ ),
98
+ );
99
+
100
+ interface ThemeGitColors {
101
+ themeName: string;
102
+ colors: CustomProperties;
103
+ }
104
+
105
+ /** Empty until the theme resolves, or when it cannot: the accents stand in. */
106
+ const useThemeGitColors = (themeName: string): CustomProperties => {
107
+ const [resolved, setResolved] = useState<ThemeGitColors | null>(null);
108
+ useEffect(() => {
109
+ let current = true;
110
+ Promise.resolve(getResolvedOrResolveTheme(themeName)).then(
111
+ (theme) => {
112
+ if (current) {
113
+ setResolved({
114
+ themeName,
115
+ colors: gitColorsOf(themeToTreeStyles(theme)),
116
+ });
117
+ }
118
+ },
119
+ () => {
120
+ if (current) setResolved({ themeName, colors: {} });
121
+ },
122
+ );
123
+ return () => {
124
+ current = false;
125
+ };
126
+ }, [themeName]);
127
+ return resolved?.themeName === themeName ? resolved.colors : {};
128
+ };
129
+
130
+ const gitStatusOf = (files: readonly DiffTreeFile[]): GitStatusEntry[] =>
131
+ files.flatMap(({ path, status }) =>
132
+ status === null ? [] : [{ path, status }],
133
+ );
134
+
135
+ const decorationOf = (
136
+ path: string,
137
+ { viewedPaths, omittedPaths }: DecorationData,
138
+ ): FileTreeRowDecoration | null => {
139
+ if (omittedPaths.has(path)) {
140
+ return { text: "left out", title: "Not in the diff: listed above it" };
141
+ }
142
+ if (viewedPaths.has(path)) return { text: "viewed", title: "Marked viewed" };
143
+ return null;
144
+ };
145
+
146
+ /**
147
+ * The range's files by folder, in `@pierre/trees`: searchable, virtualized,
148
+ * each file's git status and viewed mark on its row. Web only (DOM in a shadow
149
+ * root), loaded lazily as the diff viewer is.
150
+ *
151
+ * The model is created once and fed through its methods. Selecting a file
152
+ * scrolls the diff to it and is undone at once, so pressing the same file
153
+ * again scrolls again. The row decorations have no setter: they read the
154
+ * latest data from a ref, and resetting the git status repaints them.
155
+ */
156
+ function PullRequestDiffTree({
157
+ files,
158
+ viewedPaths: viewedList,
159
+ theme,
160
+ onFilePress,
161
+ }: PullRequestDiffTreeProps): ReactNode {
162
+ const mode = useCurrentMode();
163
+ const gitColors = useThemeGitColors(diffThemeNames(theme)[mode]);
164
+ const paths = files.map(({ path }) => path);
165
+ const pathsKey = paths.join("\n");
166
+ const repaintKey = [
167
+ ...files.map(({ path, status }) => `${status ?? "omitted"} ${path}`),
168
+ ...viewedList,
169
+ ].join("\n");
170
+ const viewedPaths = new Set(viewedList);
171
+ const omittedPaths = new Set(
172
+ files.filter(({ status }) => status === null).map(({ path }) => path),
173
+ );
174
+ const latest = useRef({ files, onFilePress, viewedPaths, omittedPaths });
175
+ const filePaths = useRef(new Set(paths));
176
+ const modelRef = useRef<FileTreeModel | null>(null);
177
+ const appliedPathsKey = useRef(pathsKey);
178
+
179
+ const { model } = useFileTree({
180
+ paths,
181
+ flattenEmptyDirectories: true,
182
+ initialExpansion: "open",
183
+ density: "compact",
184
+ icons: { set: "standard", colored: false },
185
+ search: true,
186
+ fileTreeSearchMode: "hide-non-matches",
187
+ gitStatus: gitStatusOf(files),
188
+ renderRowDecoration: ({ item }) =>
189
+ item.kind === "file" ? decorationOf(item.path, latest.current) : null,
190
+ onSelectionChange: (selected) => {
191
+ const path = selected.at(-1);
192
+ if (path === undefined || !filePaths.current.has(path)) return;
193
+ latest.current.onFilePress(path);
194
+ queueMicrotask(() => {
195
+ modelRef.current?.getItem(path)?.deselect();
196
+ });
197
+ },
198
+ });
199
+
200
+ useEffect(() => {
201
+ modelRef.current = model;
202
+ latest.current = { files, onFilePress, viewedPaths, omittedPaths };
203
+ filePaths.current = new Set(paths);
204
+ });
205
+
206
+ useEffect(() => {
207
+ const current = latest.current.files;
208
+ if (appliedPathsKey.current !== pathsKey) {
209
+ appliedPathsKey.current = pathsKey;
210
+ model.resetPaths(current.map(({ path }) => path));
211
+ }
212
+ model.setGitStatus(gitStatusOf(current));
213
+ }, [model, pathsKey, repaintKey]);
214
+
215
+ return (
216
+ <CapturedAccent accent="brand">
217
+ <CapturedAccent accent="success">
218
+ <CapturedAccent accent="info">
219
+ <CapturedAccent accent="warning">
220
+ <CapturedAccent accent="danger">
221
+ <AccentScope accent="neutral">
222
+ <FileTree model={model} style={hostStyle(mode, gitColors)} />
223
+ </AccentScope>
224
+ </CapturedAccent>
225
+ </CapturedAccent>
226
+ </CapturedAccent>
227
+ </CapturedAccent>
228
+ </CapturedAccent>
229
+ );
230
+ }
231
+
232
+ export default PullRequestDiffTree;
@@ -7,10 +7,11 @@ import type {
7
7
  import {
8
8
  AccentScope,
9
9
  Code,
10
- IconButton,
11
10
  IndeterminateCircularProgress,
12
11
  IndeterminateLinearProgress,
13
12
  InfoMessage,
13
+ Tab,
14
+ Tabs,
14
15
  Text,
15
16
  View,
16
17
  WarningMessage,
@@ -31,6 +32,7 @@ import {
31
32
  useRef,
32
33
  useState,
33
34
  } from "react";
35
+ import { Platform } from "react-native";
34
36
  import type { PullRequestDiffRefusal } from "../dataSource.ts";
35
37
  import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
36
38
  import { usePullRequestSlots } from "../slots.tsx";
@@ -51,20 +53,30 @@ import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
51
53
  import {
52
54
  DiffFileElementsContext,
53
55
  createDiffFileElements,
56
+ useFileInView,
54
57
  } from "./pull-request-diff-jump.ts";
55
58
  import type { Composer } from "./pull-request-diff-lines.ts";
56
59
  import type { OverviewReadingOrderStatus } from "./pull-request-diff-overview.tsx";
57
60
  import { PullRequestDiffOverview } from "./pull-request-diff-overview.tsx";
61
+ import { diffTreeFilesOf } from "./pull-request-diff-tree-files.ts";
58
62
  import type {
59
63
  DiffStyle,
60
64
  PullRequestDiffReview,
61
65
  } from "./pull-request-diff-viewer.tsx";
62
66
  import { cssVariableOf, scrollToTopOf } from "./scroll-parent.ts";
67
+ import { TooltipIconButton } from "./tooltip-icon-button.tsx";
63
68
 
64
69
  const PullRequestDiffViewer = lazy(
65
70
  () => import("./pull-request-diff-viewer.tsx"),
66
71
  );
67
72
 
73
+ const PullRequestDiffTree = lazy(() => import("./pull-request-diff-tree.tsx"));
74
+
75
+ /** the tree renders DOM in a shadow root */
76
+ const diffTreeAvailable = Platform.OS === "web";
77
+
78
+ type AsideView = "list" | "tree";
79
+
68
80
  const omissionLabel = (reason: DiffOmittedFile["reason"]): string => {
69
81
  if (reason === "binary") return "binary";
70
82
  if (reason === "tooLarge") return "too large";
@@ -209,6 +221,7 @@ export function PullRequestDiffView({
209
221
  const [toolbarHeight, setToolbarHeight] = useState(0);
210
222
  const fileElements = useMemo(createDiffFileElements, []);
211
223
  const highlightProbe = useRef<ComponentRef<typeof View>>(null);
224
+ const toolbar = useRef<ComponentRef<typeof View>>(null);
212
225
  // a JS breakpoint rather than `xl:` classes: the sticky offset is the measured toolbar's
213
226
  const aside = useCurrentBreakpointNameFiltered(["base", "wide"]) === "wide";
214
227
  const whitespace = useWhitespaceToggle();
@@ -244,7 +257,8 @@ export function PullRequestDiffView({
244
257
  (shownCommitComposer !== null ||
245
258
  (review.commitOid === undefined && draft.composer !== null));
246
259
  const mode = useCurrentMode();
247
- const { wordWrap, theme, fileOrder, setFileOrder } = useDiffSettings();
260
+ const { wordWrap, theme, fontFamily, fileOrder, setFileOrder } =
261
+ useDiffSettings();
248
262
  const missingCheckout = useMissingCheckoutReason(range);
249
263
  const readingOrder = usePullRequestReadingOrder(
250
264
  range,
@@ -267,6 +281,48 @@ export function PullRequestDiffView({
267
281
  if (readingOrder.error !== null) return "failed";
268
282
  return "none";
269
283
  })();
284
+ const [requestedAsideView, setRequestedAsideView] =
285
+ useState<AsideView | null>(null);
286
+ const asideView = ((): AsideView => {
287
+ if (!diffTreeAvailable) return "list";
288
+ if (requestedAsideView !== null) return requestedAsideView;
289
+ return ordered ? "list" : "tree";
290
+ })();
291
+ const listTab = (
292
+ <Tab
293
+ value="list"
294
+ label={readingOrder.available ? "Reading order" : "Workspaces"}
295
+ />
296
+ );
297
+ const treeFiles = useMemo(
298
+ () => (shownDiff === null ? [] : diffTreeFilesOf(shownDiff)),
299
+ [shownDiff],
300
+ );
301
+ const statuses = useMemo(
302
+ () =>
303
+ new Map(
304
+ treeFiles.flatMap(({ path, status }) =>
305
+ status === null ? [] : [[path, status] as const],
306
+ ),
307
+ ),
308
+ [treeFiles],
309
+ );
310
+ const viewedPaths = (review?.files ?? [])
311
+ .filter(({ viewed }) => viewed === "viewed")
312
+ .map(({ path }) => path);
313
+ const fileInView = useFileInView(
314
+ fileElements,
315
+ toolbar,
316
+ aside && asideView === "list",
317
+ );
318
+
319
+ const goToFile = (path: string): void => {
320
+ const element = fileElements.elementOf(path);
321
+ if (element === undefined) return;
322
+ scrollToTopOf(element, toolbarHeight);
323
+ const color = cssVariableOf(highlightProbe.current, "--color-enabled");
324
+ if (color !== "") fileElements.highlight(path, color);
325
+ };
270
326
 
271
327
  return (
272
328
  <View
@@ -277,6 +333,7 @@ export function PullRequestDiffView({
277
333
  }
278
334
  >
279
335
  <View
336
+ ref={toolbar}
280
337
  className="-mx-l flex-row flex-wrap items-center justify-between gap-xs bg-screen px-l py-sm web:sticky web:top-0 web:z-10"
281
338
  onLayout={(event) => {
282
339
  setToolbarHeight(event.nativeEvent.layout.height);
@@ -296,7 +353,7 @@ export function PullRequestDiffView({
296
353
  {toolbarStart}
297
354
  </View>
298
355
  <View className="flex-row items-center gap-xs">
299
- <IconButton
356
+ <TooltipIconButton
300
357
  icon={
301
358
  diffStyle === "split" ? (
302
359
  <RowsRegularIcon />
@@ -313,7 +370,7 @@ export function PullRequestDiffView({
313
370
  }}
314
371
  />
315
372
  {readingOrder.available ? (
316
- <IconButton
373
+ <TooltipIconButton
317
374
  icon={<TreeStructureRegularIcon />}
318
375
  size="sm"
319
376
  accent={ordered ? "brand" : "neutral"}
@@ -325,6 +382,7 @@ export function PullRequestDiffView({
325
382
  }
326
383
  onPress={() => {
327
384
  setFileOrder(ordered ? "packages" : "reading");
385
+ setRequestedAsideView(null);
328
386
  }}
329
387
  />
330
388
  ) : null}
@@ -335,13 +393,13 @@ export function PullRequestDiffView({
335
393
  }
336
394
  onToggle={whitespace.toggle}
337
395
  />
338
- <IconButton
396
+ <TooltipIconButton
339
397
  icon={<ArrowsOutLineVerticalRegularIcon />}
340
398
  size="sm"
341
399
  aria-label="Open every file"
342
400
  onPress={collapse.expandAll}
343
401
  />
344
- <IconButton
402
+ <TooltipIconButton
345
403
  icon={<ArrowsInLineVerticalRegularIcon />}
346
404
  size="sm"
347
405
  aria-label="Close every file"
@@ -369,31 +427,56 @@ export function PullRequestDiffView({
369
427
  <View
370
428
  className={
371
429
  aside
372
- ? "w-[280px] shrink-0 web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
430
+ ? "w-[280px] shrink-0 gap-sm web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
373
431
  : ""
374
432
  }
375
433
  style={aside ? { top: toolbarHeight } : undefined}
376
434
  >
377
- <PullRequestDiffOverview
378
- groups={shownDiff.groups}
379
- omitted={shownDiff.omitted}
380
- readingOrder={readingOrder.value}
381
- readingOrderStatus={overviewStatus}
382
- impacts={impacts.value}
383
- impactsStatus={impactsStatus}
384
- displayOrder={ordered ? readingOrder.value : null}
385
- aside={aside}
386
- onFilePress={(path) => {
387
- const element = fileElements.elementOf(path);
388
- if (element === undefined) return;
389
- scrollToTopOf(element, toolbarHeight);
390
- const color = cssVariableOf(
391
- highlightProbe.current,
392
- "--color-enabled",
393
- );
394
- if (color !== "") fileElements.highlight(path, color);
395
- }}
396
- />
435
+ {aside && diffTreeAvailable ? (
436
+ <Tabs
437
+ aria-label="Files listed"
438
+ value={asideView}
439
+ onValueChange={(value) => {
440
+ setRequestedAsideView(value as AsideView);
441
+ }}
442
+ >
443
+ {ordered ? listTab : null}
444
+ <Tab value="tree" label="Tree" />
445
+ {ordered ? null : listTab}
446
+ </Tabs>
447
+ ) : null}
448
+ {aside && asideView === "tree" ? (
449
+ // a height of its own: the tree virtualizes, and grows to every row without one
450
+ <View className="web:h-[calc(100vh-12rem)]">
451
+ <Suspense fallback={<DiffLoading />}>
452
+ <PullRequestDiffTree
453
+ files={treeFiles}
454
+ viewedPaths={viewedPaths}
455
+ theme={theme}
456
+ onFilePress={goToFile}
457
+ />
458
+ </Suspense>
459
+ </View>
460
+ ) : (
461
+ <PullRequestDiffOverview
462
+ groups={shownDiff.groups}
463
+ omitted={shownDiff.omitted}
464
+ readingOrder={readingOrder.value}
465
+ readingOrderStatus={overviewStatus}
466
+ impacts={impacts.value}
467
+ impactsStatus={impactsStatus}
468
+ displayOrder={
469
+ (aside && diffTreeAvailable) || ordered
470
+ ? readingOrder.value
471
+ : null
472
+ }
473
+ statuses={statuses}
474
+ viewedPaths={viewedPaths}
475
+ currentPath={fileInView}
476
+ aside={aside}
477
+ onFilePress={goToFile}
478
+ />
479
+ )}
397
480
  {/* the file header is pierre's shadow DOM, which no accent
398
481
  class reaches: the color is read here and handed to it */}
399
482
  <AccentScope accent="info">
@@ -418,6 +501,7 @@ export function PullRequestDiffView({
418
501
  diffStyle={diffStyle}
419
502
  theme={theme}
420
503
  themeType={mode}
504
+ fontFamily={fontFamily}
421
505
  wordWrap={wordWrap}
422
506
  stickyTop={toolbarHeight}
423
507
  collapse={collapse}
@@ -49,11 +49,16 @@ import type { PullRequestDataSource } from "../dataSource.ts";
49
49
  import { usePullRequestDataSource } from "../dataSource.ts";
50
50
  import type { PendingComment } from "../services/PullRequestReviewDraftProvider.tsx";
51
51
  import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
52
+ import { diffFontCSS } from "./branch-diff/diff-font.ts";
52
53
  import type { DiffTheme } from "./branch-diff/diff-themes.ts";
53
- import { diffThemeNames } from "./branch-diff/diff-themes.ts";
54
+ import {
55
+ changeBackgroundCSS,
56
+ diffThemeNames,
57
+ } from "./branch-diff/diff-themes.ts";
54
58
  // metro picks the .web.tsx variant only for a specifier without extension
55
59
  /* eslint-disable-next-line import-x/no-unresolved */ // oxlint-disable-next-line import/extensions
56
60
  import { DiffWorkerPool } from "./branch-diff/diff-worker-pool";
61
+ import { reviewflowSelectionColors } from "./branch-diff/reviewflow-diff-selection.ts";
57
62
  import type { CommitFileAnchoring } from "./pull-request-commit-files.ts";
58
63
  import type {
59
64
  AnnotationGroup,
@@ -428,6 +433,13 @@ const fileRadius = "1rem";
428
433
  */
429
434
  const roundedFile = `:host { border-radius: ${fileRadius}; overflow: clip; box-shadow: ${surfaceShadow}; }`;
430
435
 
436
+ /** The reviewflow theme's selected lines mix with a lighter blue than its modified color. */
437
+ const selectionCSS = (theme: DiffTheme, mode: "dark" | "light"): string => {
438
+ if (theme !== "reviewflow") return "";
439
+ const color = reviewflowSelectionColors[mode];
440
+ return `:host { --diffs-bg-selection-override: ${color}; --diffs-bg-selection-number-override: ${color}; }`;
441
+ };
442
+
431
443
  const pierreScrollbars =
432
444
  "@supports not (-moz-appearance: none) { [data-code] { scrollbar-width: auto; scrollbar-color: auto; } }";
433
445
 
@@ -446,6 +458,7 @@ interface DiffFileProps {
446
458
  diffStyle: DiffStyle;
447
459
  theme: DiffTheme;
448
460
  themeType: "dark" | "light";
461
+ fontFamily: string | null;
449
462
  wordWrap: boolean;
450
463
  stickyTop: number;
451
464
  collapsed: boolean;
@@ -634,6 +647,7 @@ function DiffFile({
634
647
  diffStyle,
635
648
  theme,
636
649
  themeType,
650
+ fontFamily,
637
651
  wordWrap,
638
652
  stickyTop,
639
653
  collapsed,
@@ -722,7 +736,7 @@ function DiffFile({
722
736
  overflow: wordWrap ? "wrap" : "scroll",
723
737
  collapsed,
724
738
  stickyHeader: true,
725
- unsafeCSS: `${stickyHeaderBelow(stickyTop)} ${headerInset} ${roundedFile} ${pierreScrollbars} ${headerHighlight(highlight, collapsed)}`,
739
+ unsafeCSS: `${stickyHeaderBelow(stickyTop)} ${headerInset} ${roundedFile} ${pierreScrollbars} ${diffFontCSS(fontFamily)} ${selectionCSS(theme, themeType)} ${changeBackgroundCSS(theme)} ${headerHighlight(highlight, collapsed)}`,
726
740
  loadDiffFiles: loadFiles,
727
741
  enableGutterUtility: commentable,
728
742
  onGutterUtilityClick: commentable ? onGutterUtilityClick : undefined,
@@ -1018,6 +1032,7 @@ interface ReviewedFilesProps {
1018
1032
  diffStyle: DiffStyle;
1019
1033
  theme: DiffTheme;
1020
1034
  themeType: "dark" | "light";
1035
+ fontFamily: string | null;
1021
1036
  wordWrap: boolean;
1022
1037
  stickyTop: number;
1023
1038
  collapse: PullRequestCollapse;
@@ -1034,6 +1049,7 @@ function ReviewedFiles({
1034
1049
  diffStyle,
1035
1050
  theme,
1036
1051
  themeType,
1052
+ fontFamily,
1037
1053
  wordWrap,
1038
1054
  stickyTop,
1039
1055
  collapse,
@@ -1174,6 +1190,7 @@ function ReviewedFiles({
1174
1190
  diffStyle={diffStyle}
1175
1191
  theme={theme}
1176
1192
  themeType={themeType}
1193
+ fontFamily={fontFamily}
1177
1194
  wordWrap={wordWrap}
1178
1195
  stickyTop={stickyTop}
1179
1196
  loadFiles={loadFiles}
@@ -1249,6 +1266,7 @@ interface PullRequestDiffViewerProps {
1249
1266
  diffStyle: DiffStyle;
1250
1267
  theme: DiffTheme;
1251
1268
  themeType: "dark" | "light";
1269
+ fontFamily: string | null;
1252
1270
  wordWrap: boolean;
1253
1271
  /** where a file header sticks: the height of the toolbar stuck above it */
1254
1272
  stickyTop: number;
@@ -1282,6 +1300,7 @@ function PullRequestDiffViewer({
1282
1300
  diffStyle,
1283
1301
  theme,
1284
1302
  themeType,
1303
+ fontFamily,
1285
1304
  wordWrap,
1286
1305
  stickyTop,
1287
1306
  collapse,
@@ -1334,6 +1353,7 @@ function PullRequestDiffViewer({
1334
1353
  diffStyle={diffStyle}
1335
1354
  theme={theme}
1336
1355
  themeType={themeType}
1356
+ fontFamily={fontFamily}
1337
1357
  wordWrap={wordWrap}
1338
1358
  stickyTop={stickyTop}
1339
1359
  loadFiles={loadFiles}
@@ -1357,6 +1377,7 @@ function PullRequestDiffViewer({
1357
1377
  diffStyle={diffStyle}
1358
1378
  theme={theme}
1359
1379
  themeType={themeType}
1380
+ fontFamily={fontFamily}
1360
1381
  wordWrap={wordWrap}
1361
1382
  stickyTop={stickyTop}
1362
1383
  collapse={collapse}