@reviewflow/ui 6.23.0 → 6.25.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 (39) hide show
  1. package/package.json +21 -6
  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-actions-review.tsx +2 -2
  13. package/src/components/pull-request-commits.tsx +2 -1
  14. package/src/components/pull-request-diff-annotations.tsx +8 -5
  15. package/src/components/pull-request-diff-impacts-summary.tsx +2 -1
  16. package/src/components/pull-request-diff-impacts.ts +1 -3
  17. package/src/components/pull-request-diff-jump.ts +59 -1
  18. package/src/components/pull-request-diff-overview-tree.ts +57 -0
  19. package/src/components/pull-request-diff-overview.tsx +489 -90
  20. package/src/components/pull-request-diff-tree-files.ts +64 -0
  21. package/src/components/pull-request-diff-tree.tsx +232 -0
  22. package/src/components/pull-request-diff-view.tsx +112 -28
  23. package/src/components/pull-request-diff-viewer.tsx +26 -6
  24. package/src/components/pull-request-files.tsx +6 -6
  25. package/src/components/pull-request-panel.tsx +23 -21
  26. package/src/components/pull-request-reactions.tsx +2 -2
  27. package/src/components/pull-request-revisions.tsx +2 -1
  28. package/src/components/pull-request-screen.tsx +37 -17
  29. package/src/components/pull-request-suggestion-actions.tsx +7 -6
  30. package/src/components/repository-attachment-banner.tsx +1 -1
  31. package/src/components/scroll-parent.ts +20 -0
  32. package/src/components/tooltip-icon-button.tsx +27 -0
  33. package/src/editor/code-editor.tsx +1 -0
  34. package/src/editor/code-token-colors.css +39 -0
  35. package/src/editor/codeHighlight.ts +2 -1
  36. package/src/editor/markdown-editor.css +1 -34
  37. package/src/editor/markdown-editor.tsx +1 -0
  38. package/src/index.ts +6 -6
  39. package/src/palette.ts +22 -0
@@ -18,6 +18,7 @@ import type {
18
18
  PullRequestRef,
19
19
  PullRequestThread,
20
20
  } from "@reviewflow/sdk";
21
+ import { pluralOf } from "@reviewflow/sdk";
21
22
  import {
22
23
  AccentScope,
23
24
  Badge,
@@ -48,11 +49,16 @@ import type { PullRequestDataSource } from "../dataSource.ts";
48
49
  import { usePullRequestDataSource } from "../dataSource.ts";
49
50
  import type { PendingComment } from "../services/PullRequestReviewDraftProvider.tsx";
50
51
  import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
52
+ import { diffFontCSS } from "./branch-diff/diff-font.ts";
51
53
  import type { DiffTheme } from "./branch-diff/diff-themes.ts";
52
- import { diffThemeNames } from "./branch-diff/diff-themes.ts";
54
+ import {
55
+ changeBackgroundCSS,
56
+ diffThemeNames,
57
+ } from "./branch-diff/diff-themes.ts";
53
58
  // metro picks the .web.tsx variant only for a specifier without extension
54
59
  /* eslint-disable-next-line import-x/no-unresolved */ // oxlint-disable-next-line import/extensions
55
60
  import { DiffWorkerPool } from "./branch-diff/diff-worker-pool";
61
+ import { reviewflowSelectionColors } from "./branch-diff/reviewflow-diff-selection.ts";
56
62
  import type { CommitFileAnchoring } from "./pull-request-commit-files.ts";
57
63
  import type {
58
64
  AnnotationGroup,
@@ -70,7 +76,6 @@ import type { PullRequestMovedOn } from "./pull-request-diff-data.ts";
70
76
  import {
71
77
  countImpacts,
72
78
  maxImporterNames,
73
- pluralOf,
74
79
  usageLabel,
75
80
  } from "./pull-request-diff-impacts.ts";
76
81
  import type { DiffFileHighlight } from "./pull-request-diff-jump.ts";
@@ -214,8 +219,7 @@ const useStableAnnotations = (
214
219
  /** Why a thread is listed above its file rather than placed on a line. */
215
220
  type UnplacedKind = "changedSince" | "outdated" | "whitespace";
216
221
 
217
- const commentsText = (count: number): string =>
218
- `${String(count)} comment${count === 1 ? "" : "s"}`;
222
+ const commentsText = (count: number): string => pluralOf(count, "comment");
219
223
 
220
224
  const unplacedLabel = (kind: UnplacedKind, count: number): string => {
221
225
  if (kind === "changedSince") {
@@ -224,7 +228,7 @@ const unplacedLabel = (kind: UnplacedKind, count: number): string => {
224
228
  if (kind === "whitespace") {
225
229
  return `${commentsText(count)} on lines hidden with whitespace changes`;
226
230
  }
227
- return `${String(count)} outdated comment${count === 1 ? "" : "s"}`;
231
+ return pluralOf(count, "outdated comment");
228
232
  };
229
233
 
230
234
  const unplacedLineText = (
@@ -429,6 +433,13 @@ const fileRadius = "1rem";
429
433
  */
430
434
  const roundedFile = `:host { border-radius: ${fileRadius}; overflow: clip; box-shadow: ${surfaceShadow}; }`;
431
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
+
432
443
  const pierreScrollbars =
433
444
  "@supports not (-moz-appearance: none) { [data-code] { scrollbar-width: auto; scrollbar-color: auto; } }";
434
445
 
@@ -447,6 +458,7 @@ interface DiffFileProps {
447
458
  diffStyle: DiffStyle;
448
459
  theme: DiffTheme;
449
460
  themeType: "dark" | "light";
461
+ fontFamily: string | null;
450
462
  wordWrap: boolean;
451
463
  stickyTop: number;
452
464
  collapsed: boolean;
@@ -635,6 +647,7 @@ function DiffFile({
635
647
  diffStyle,
636
648
  theme,
637
649
  themeType,
650
+ fontFamily,
638
651
  wordWrap,
639
652
  stickyTop,
640
653
  collapsed,
@@ -723,7 +736,7 @@ function DiffFile({
723
736
  overflow: wordWrap ? "wrap" : "scroll",
724
737
  collapsed,
725
738
  stickyHeader: true,
726
- 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)}`,
727
740
  loadDiffFiles: loadFiles,
728
741
  enableGutterUtility: commentable,
729
742
  onGutterUtilityClick: commentable ? onGutterUtilityClick : undefined,
@@ -1019,6 +1032,7 @@ interface ReviewedFilesProps {
1019
1032
  diffStyle: DiffStyle;
1020
1033
  theme: DiffTheme;
1021
1034
  themeType: "dark" | "light";
1035
+ fontFamily: string | null;
1022
1036
  wordWrap: boolean;
1023
1037
  stickyTop: number;
1024
1038
  collapse: PullRequestCollapse;
@@ -1035,6 +1049,7 @@ function ReviewedFiles({
1035
1049
  diffStyle,
1036
1050
  theme,
1037
1051
  themeType,
1052
+ fontFamily,
1038
1053
  wordWrap,
1039
1054
  stickyTop,
1040
1055
  collapse,
@@ -1175,6 +1190,7 @@ function ReviewedFiles({
1175
1190
  diffStyle={diffStyle}
1176
1191
  theme={theme}
1177
1192
  themeType={themeType}
1193
+ fontFamily={fontFamily}
1178
1194
  wordWrap={wordWrap}
1179
1195
  stickyTop={stickyTop}
1180
1196
  loadFiles={loadFiles}
@@ -1250,6 +1266,7 @@ interface PullRequestDiffViewerProps {
1250
1266
  diffStyle: DiffStyle;
1251
1267
  theme: DiffTheme;
1252
1268
  themeType: "dark" | "light";
1269
+ fontFamily: string | null;
1253
1270
  wordWrap: boolean;
1254
1271
  /** where a file header sticks: the height of the toolbar stuck above it */
1255
1272
  stickyTop: number;
@@ -1283,6 +1300,7 @@ function PullRequestDiffViewer({
1283
1300
  diffStyle,
1284
1301
  theme,
1285
1302
  themeType,
1303
+ fontFamily,
1286
1304
  wordWrap,
1287
1305
  stickyTop,
1288
1306
  collapse,
@@ -1335,6 +1353,7 @@ function PullRequestDiffViewer({
1335
1353
  diffStyle={diffStyle}
1336
1354
  theme={theme}
1337
1355
  themeType={themeType}
1356
+ fontFamily={fontFamily}
1338
1357
  wordWrap={wordWrap}
1339
1358
  stickyTop={stickyTop}
1340
1359
  loadFiles={loadFiles}
@@ -1358,6 +1377,7 @@ function PullRequestDiffViewer({
1358
1377
  diffStyle={diffStyle}
1359
1378
  theme={theme}
1360
1379
  themeType={themeType}
1380
+ fontFamily={fontFamily}
1361
1381
  wordWrap={wordWrap}
1362
1382
  stickyTop={stickyTop}
1363
1383
  collapse={collapse}
@@ -6,11 +6,10 @@ import type {
6
6
  PullRequestThread,
7
7
  PullRequestViewerLastReview,
8
8
  } from "@reviewflow/sdk";
9
- import { formatRelativeDate } from "@reviewflow/sdk";
9
+ import { formatRelativeDate, pluralOf } from "@reviewflow/sdk";
10
10
  import {
11
11
  Button,
12
12
  Code,
13
- IconButton,
14
13
  InfoMessage,
15
14
  Menu,
16
15
  MenuItem,
@@ -54,6 +53,7 @@ import {
54
53
  PullRequestPartPending,
55
54
  PullRequestPartReading,
56
55
  } from "./pull-request-part-notice.tsx";
56
+ import { TooltipIconButton } from "./tooltip-icon-button.tsx";
57
57
 
58
58
  type Scope = "all" | "since";
59
59
 
@@ -130,7 +130,7 @@ function ByCommitButton({
130
130
  onToggle,
131
131
  }: ByCommitButtonProps): ReactNode {
132
132
  return (
133
- <IconButton
133
+ <TooltipIconButton
134
134
  icon={<GitCommitRegularIcon />}
135
135
  size="sm"
136
136
  accent={byCommit ? "brand" : "neutral"}
@@ -149,7 +149,7 @@ const describeCommit = (
149
149
  ): string => {
150
150
  const oid = commit.oid.slice(0, 7);
151
151
  if (summary === undefined) return oid;
152
- const files = `${String(summary.files)} ${summary.files === 1 ? "file" : "files"}`;
152
+ const files = pluralOf(summary.files, "file");
153
153
  return [oid, summary.workspaces.join(", "), files]
154
154
  .filter((part) => part !== "")
155
155
  .join(" · ");
@@ -173,7 +173,7 @@ function CommitStepper({
173
173
  if (commit === undefined) return null;
174
174
  return (
175
175
  <View className="min-w-0 shrink flex-row items-center gap-xs">
176
- <IconButton
176
+ <TooltipIconButton
177
177
  icon={<CaretLeftRegularIcon />}
178
178
  size="sm"
179
179
  accent="neutral"
@@ -207,7 +207,7 @@ function CommitStepper({
207
207
  />
208
208
  ))}
209
209
  </Menu>
210
- <IconButton
210
+ <TooltipIconButton
211
211
  icon={<CaretRightRegularIcon />}
212
212
  size="sm"
213
213
  accent="neutral"
@@ -7,7 +7,12 @@ import type {
7
7
  PullRequestReviewState,
8
8
  PullRequestSummary,
9
9
  } from "@reviewflow/sdk";
10
- import { prStatus, prStatusLabels, reviewflowPrStatus } from "@reviewflow/sdk";
10
+ import {
11
+ pluralOf,
12
+ prStatus,
13
+ prStatusLabels,
14
+ reviewflowPrStatus,
15
+ } from "@reviewflow/sdk";
11
16
  import type { Accent } from "alouette";
12
17
  import {
13
18
  AccentScope,
@@ -39,6 +44,7 @@ import { prStatusAccents } from "../prStatus.ts";
39
44
  import { emojify } from "../services/emojify.ts";
40
45
  import { errorToMessage } from "../services/errorToMessage.ts";
41
46
  import { formatDataAge } from "../services/formatDataAge.ts";
47
+ import { isViewerAuthor } from "../services/isViewerAuthor.ts";
42
48
  import { LabelPill } from "./label-pill.tsx";
43
49
  import {
44
50
  PullRequestMergeButton,
@@ -48,7 +54,6 @@ import {
48
54
  import { CheckLine } from "./pull-request-checks.tsx";
49
55
  import { PullRequestCommits } from "./pull-request-commits.tsx";
50
56
  import { PersonAvatar } from "./pull-request-conversation-person.tsx";
51
- import { PullRequestFiles } from "./pull-request-files.tsx";
52
57
  import {
53
58
  PullRequestPartError,
54
59
  PullRequestPartPending,
@@ -361,7 +366,7 @@ function UnresolvedThreads({
361
366
  </View>
362
367
  </AccentScope>
363
368
  <Text className="min-w-0 flex-1 text-sm">
364
- {`${String(unresolved)} unresolved ${unresolved === 1 ? "thread" : "threads"}`}
369
+ {pluralOf(unresolved, "unresolved thread")}
365
370
  </Text>
366
371
  </View>
367
372
  );
@@ -1120,18 +1125,12 @@ function Sidebar({ detail, candidates, onEdited }: CandidatesProps): ReactNode {
1120
1125
  );
1121
1126
  }
1122
1127
 
1123
- export type PullRequestTab =
1124
- | "checks"
1125
- | "commits"
1126
- | "files"
1127
- | "review"
1128
- | "timeline";
1128
+ export type PullRequestTab = "checks" | "commits" | "review" | "timeline";
1129
1129
 
1130
1130
  export const isPullRequestTab = (value: unknown): value is PullRequestTab =>
1131
1131
  value === "checks" ||
1132
1132
  value === "commits" ||
1133
1133
  value === "timeline" ||
1134
- value === "files" ||
1135
1134
  value === "review";
1136
1135
 
1137
1136
  /** The timeline opens on the description, as GitHub's first comment. */
@@ -1164,7 +1163,8 @@ function PullRequestActions({ detail, onEdited }: EditableProps): ReactNode {
1164
1163
  export interface PullRequestPanelProps extends CandidatesProps {
1165
1164
  /** why the editors stay read-only, when GitHub refused what they offer */
1166
1165
  candidatesError: string | null;
1167
- tab: PullRequestTab;
1166
+ /** absent: the first tab, Review for a reviewer and Conversation for the author */
1167
+ tab?: PullRequestTab;
1168
1168
  onTabChange: (tab: PullRequestTab) => void;
1169
1169
  /** the commit whose diff is open on the Commits tab; absent: the tab holds it */
1170
1170
  openCommit?: string | null;
@@ -1174,7 +1174,7 @@ export interface PullRequestPanelProps extends CandidatesProps {
1174
1174
  /**
1175
1175
  * One pull request as GitHub reports it now, laid out as GitHub's own page —
1176
1176
  * the header, then Timeline (the description, a status box and a narrow
1177
- * sidebar), Checks and Files changed — with the edits the operator makes
1177
+ * sidebar), Commits, Checks and Review — with the edits the operator makes
1178
1178
  * before asking for a review. Every edit is GitHub's to refuse, and the panel
1179
1179
  * re-reads the pull request after it, so nothing on screen is the request
1180
1180
  * echoed back.
@@ -1184,7 +1184,7 @@ export function PullRequestPanel({
1184
1184
  candidates,
1185
1185
  candidatesError,
1186
1186
  onEdited,
1187
- tab,
1187
+ tab: tabProp,
1188
1188
  onTabChange,
1189
1189
  openCommit,
1190
1190
  onOpenCommitChange,
@@ -1194,6 +1194,14 @@ export function PullRequestPanel({
1194
1194
  pullRequest,
1195
1195
  "checks",
1196
1196
  ).value;
1197
+ const viewerIsAuthor = isViewerAuthor(detail);
1198
+ const tab = tabProp ?? (viewerIsAuthor ? "timeline" : "review");
1199
+ const reviewTab = (
1200
+ <Tab
1201
+ value="review"
1202
+ label={`Review (${pluralOf(pullRequest.changedFiles, "file")})`}
1203
+ />
1204
+ );
1197
1205
  return (
1198
1206
  <View className="gap-l">
1199
1207
  {[...new Set([...errors, ...(candidates?.errors ?? [])])].map((error) => (
@@ -1214,6 +1222,7 @@ export function PullRequestPanel({
1214
1222
  if (isPullRequestTab(next)) onTabChange(next);
1215
1223
  }}
1216
1224
  >
1225
+ {viewerIsAuthor ? null : reviewTab}
1217
1226
  <Tab value="timeline" label="Conversation" />
1218
1227
  <Tab
1219
1228
  value="commits"
@@ -1227,11 +1236,7 @@ export function PullRequestPanel({
1227
1236
  : `Checks (${String(checks.checksTotal)})`
1228
1237
  }
1229
1238
  />
1230
- <Tab
1231
- value="files"
1232
- label={`Files changed (${String(pullRequest.changedFiles)})`}
1233
- />
1234
- <Tab value="review" label="Review" />
1239
+ {viewerIsAuthor ? reviewTab : null}
1235
1240
  </Tabs>
1236
1241
  <PullRequestActions detail={detail} onEdited={onEdited} />
1237
1242
  </View>
@@ -1247,9 +1252,6 @@ export function PullRequestPanel({
1247
1252
  );
1248
1253
  }
1249
1254
  if (tab === "checks") return <ChecksTab pullRequest={pullRequest} />;
1250
- if (tab === "files") {
1251
- return <PullRequestFiles detail={detail} onEdited={onEdited} />;
1252
- }
1253
1255
  if (tab === "review") {
1254
1256
  return <PullRequestReview detail={detail} onEdited={onEdited} />;
1255
1257
  }
@@ -1,4 +1,4 @@
1
- import { pullRequestReactionContents } from "@reviewflow/sdk";
1
+ import { pluralOf, pullRequestReactionContents } from "@reviewflow/sdk";
2
2
  import type {
3
3
  PullRequestEdit,
4
4
  PullRequestReactable,
@@ -58,7 +58,7 @@ function ReactionButton({
58
58
  toggle,
59
59
  }: ReactionButtonProps): ReactNode {
60
60
  const { content, count, viewerHasReacted } = reaction;
61
- const people = count === 1 ? "1 person" : `${String(count)} people`;
61
+ const people = pluralOf(count, "person", "people");
62
62
  return (
63
63
  <ActionButton
64
64
  text={`${reactionEmoji[content]} ${String(count)}`}
@@ -3,6 +3,7 @@ import type {
3
3
  PullRequestRevision,
4
4
  PullRequestTextLine,
5
5
  } from "@reviewflow/sdk";
6
+ import { pluralOf } from "@reviewflow/sdk";
6
7
  import {
7
8
  Box,
8
9
  Button,
@@ -86,7 +87,7 @@ function ChangeRunText({ run }: { run: ChangeRun }): ReactNode {
86
87
  if (run.kind === "folded") {
87
88
  return (
88
89
  <Text className="py-xxs text-xs text-muted">
89
- {`⋯ ${String(run.count)} unchanged ${run.count === 1 ? "line" : "lines"}`}
90
+ {`⋯ ${pluralOf(run.count, "unchanged line")}`}
90
91
  </Text>
91
92
  );
92
93
  }
@@ -1,5 +1,6 @@
1
1
  import { pullRequestPartsStaleAfter } from "@reviewflow/sdk";
2
2
  import type {
3
+ DiffFont,
3
4
  DiffWhitespace,
4
5
  PullRequestDetailResponse,
5
6
  PullRequestEdit,
@@ -63,7 +64,7 @@ interface PullRequestBodyProps {
63
64
  ownerRepo: string;
64
65
  number: number;
65
66
  headerStart?: ReactNode;
66
- tab: PullRequestTab;
67
+ tab?: PullRequestTab;
67
68
  onTabChange: (tab: PullRequestTab) => void;
68
69
  openCommit?: string | null;
69
70
  onOpenCommitChange?: (oid: string | null) => void;
@@ -203,8 +204,11 @@ function PullRequestBody({
203
204
  export interface PullRequestScreenProps {
204
205
  ownerRepo: string;
205
206
  number: number;
206
- /** the tab shown; controlled, so an app can keep it in its url */
207
- tab: PullRequestTab;
207
+ /**
208
+ * The tab shown; controlled, so an app can keep it in its url. Absent: the
209
+ * first tab, Review for a reviewer and Conversation for the author.
210
+ */
211
+ tab?: PullRequestTab;
208
212
  onTabChange: (tab: PullRequestTab) => void;
209
213
  /**
210
214
  * The commit whose diff is open on the Commits tab, by oid. Absent: the
@@ -239,8 +243,12 @@ export interface PullRequestScreenProps {
239
243
  * Absent: they live as long as the screen.
240
244
  */
241
245
  draftStore?: DraftStore;
242
- /** the diff's syntax theme family; default `github` */
246
+ /** the diff's syntax theme family; default `reviewflow` */
243
247
  diffTheme?: DiffTheme;
248
+ /** the diff's code font; default `alouette` */
249
+ diffFont?: DiffFont;
250
+ /** the CSS `font-family` of the `custom` diff font */
251
+ diffCustomFont?: string;
244
252
  /** wrap long lines in the diff; default true */
245
253
  diffWordWrap?: boolean;
246
254
  /** whether the diff starts with whitespace changes hidden; the toolbar toggle then owns it */
@@ -253,11 +261,17 @@ export interface PullRequestScreenProps {
253
261
 
254
262
  /**
255
263
  * One pull request laid out as GitHub's page: the header, then the tabs
256
- * Conversation, Commits, Checks and Files changed, with the edits a reviewer
264
+ * Conversation, Commits, Checks and Review, with the edits a reviewer
257
265
  * or an author makes. Reads and writes through the
258
266
  * {@link PullRequestDataSource} of the nearest
259
267
  * `PullRequestDataSourceProvider`, which the app provides. Mount it with a
260
268
  * `key` of the pull request, so a draft review never leaks to the next one.
269
+ *
270
+ * It fills its parent, and the app chooses how wide that is: its text stays in
271
+ * a 1200px column, and only a split diff beside its file list widens to the
272
+ * whole screen. The screen is the CSS container that diff is sized against
273
+ * (`100cqw`): the window (`100vw`) overflows an app that lays the page beside a
274
+ * sidebar.
261
275
  */
262
276
  export function PullRequestScreen({
263
277
  ownerRepo,
@@ -265,6 +279,8 @@ export function PullRequestScreen({
265
279
  slots = {},
266
280
  draftStore,
267
281
  diffTheme,
282
+ diffFont,
283
+ diffCustomFont,
268
284
  diffWordWrap,
269
285
  initialWhitespace,
270
286
  diffFileOrder,
@@ -275,23 +291,27 @@ export function PullRequestScreen({
275
291
  <PullRequestSlotsProvider value={slots}>
276
292
  <DiffSettingsProvider
277
293
  theme={diffTheme}
294
+ font={diffFont}
295
+ customFont={diffCustomFont}
278
296
  wordWrap={diffWordWrap}
279
297
  initialWhitespace={initialWhitespace}
280
298
  fileOrder={diffFileOrder}
281
299
  onFileOrderChange={onDiffFileOrderChange}
282
300
  >
283
- <View className="mx-auto w-full max-w-[1200px] gap-m px-l pt-l pb-xl">
284
- <PullRequestDraftsProvider
285
- store={draftStore}
286
- pullRequest={{ ownerRepo, number }}
287
- >
288
- <PullRequestBody
289
- key={`${ownerRepo}#${String(number)}`}
290
- ownerRepo={ownerRepo}
291
- number={number}
292
- {...body}
293
- />
294
- </PullRequestDraftsProvider>
301
+ <View className="w-full px-l pt-l pb-xl web:@container">
302
+ <View className="mx-auto w-full max-w-[1200px] gap-m">
303
+ <PullRequestDraftsProvider
304
+ store={draftStore}
305
+ pullRequest={{ ownerRepo, number }}
306
+ >
307
+ <PullRequestBody
308
+ key={`${ownerRepo}#${String(number)}`}
309
+ ownerRepo={ownerRepo}
310
+ number={number}
311
+ {...body}
312
+ />
313
+ </PullRequestDraftsProvider>
314
+ </View>
295
315
  </View>
296
316
  </DiffSettingsProvider>
297
317
  </PullRequestSlotsProvider>
@@ -1,13 +1,14 @@
1
- import {
2
- parseSuggestions,
3
- suggestionCommitRefusal,
4
- suggestionRangesOverlap,
5
- } from "@reviewflow/sdk";
6
1
  import type {
7
2
  PullRequestComment,
8
3
  PullRequestSuggestionRef,
9
4
  PullRequestThread,
10
5
  } from "@reviewflow/sdk";
6
+ import {
7
+ parseSuggestions,
8
+ pluralOf,
9
+ suggestionCommitRefusal,
10
+ suggestionRangesOverlap,
11
+ } from "@reviewflow/sdk";
11
12
  import {
12
13
  Button,
13
14
  FormItem,
@@ -161,7 +162,7 @@ export function SuggestionActions({
161
162
  text={
162
163
  batch.batch.length === 0
163
164
  ? "Commit suggestion"
164
- : `Commit ${String(batch.batch.length)} suggestion${batch.batch.length === 1 ? "" : "s"}`
165
+ : `Commit ${pluralOf(batch.batch.length, "suggestion")}`
165
166
  }
166
167
  disabledReason={
167
168
  toCommit.length === 0 ? (refusal ?? undefined) : undefined
@@ -13,7 +13,7 @@ interface RepositoryAttachmentBannerProps {
13
13
  /**
14
14
  * Says up front, on every tab, that the diff of this repository cannot be read
15
15
  * because no local checkout is attached to it, rather than leaving that to be
16
- * found in Files changed. An entry that exists but fails its check shows why.
16
+ * found in the Review tab. An entry that exists but fails its check shows why.
17
17
  * Nothing when the checkout is attached.
18
18
  */
19
19
  export function RepositoryAttachmentBanner({
@@ -41,6 +41,26 @@ export const scrollToTopOf = (
41
41
  container.scrollTop -= hiddenAbove(element, container, stickyTop);
42
42
  };
43
43
 
44
+ /**
45
+ * Scrolls `element`'s scroll parent alone, the least that shows it whole
46
+ * below the `stickyTop` pixels a pinned row covers: the page around it stays
47
+ * where the reader has it, unlike `scrollIntoView`.
48
+ */
49
+ export const revealWithinScrollParent = (
50
+ element: HTMLElement,
51
+ stickyTop: number,
52
+ ): void => {
53
+ const container = scrollParentOf(element);
54
+ if (container === null) return;
55
+ const bounds = container.getBoundingClientRect();
56
+ const { top, bottom } = element.getBoundingClientRect();
57
+ const hidden = bounds.top + stickyTop - top;
58
+ if (hidden > 0) container.scrollTop -= hidden;
59
+ else if (bottom > bounds.bottom) {
60
+ container.scrollTop += bottom - bounds.bottom;
61
+ }
62
+ };
63
+
44
64
  /** As `scrollToTopOf`, only when `element`'s top is hidden above that bar. */
45
65
  export const revealTopOf = (element: HTMLElement, stickyTop: number): void => {
46
66
  const container = scrollParentOf(element);
@@ -0,0 +1,27 @@
1
+ import type { IconButtonProps } from "alouette";
2
+ import { IconButton, Tooltip } from "alouette";
3
+ import type { ReactNode } from "react";
4
+
5
+ /**
6
+ * An icon-only button whose label shows on hover, so its purpose is not left
7
+ * to the glyph. A disabled one shows its reason instead, in the tooltip the
8
+ * button renders itself. To replace by alouette's own once released:
9
+ * https://github.com/christophehurpeau/alouette/pull/448
10
+ */
11
+ export function TooltipIconButton(props: IconButtonProps): ReactNode {
12
+ const { disabledReason, "aria-label": label } = props;
13
+ return (
14
+ <Tooltip
15
+ content={disabledReason ? null : label}
16
+ // the label already names the button: as its description too, a screen reader reads it twice
17
+ render={(triggerProps) => (
18
+ <IconButton
19
+ {...props}
20
+ {...triggerProps}
21
+ aria-describedby={undefined}
22
+ accessibilityHint={undefined}
23
+ />
24
+ )}
25
+ />
26
+ );
27
+ }
@@ -6,6 +6,7 @@ import type { CodeLanguage } from "./codeEditorExtensions.ts";
6
6
  import { codeEditorExtensions } from "./codeEditorExtensions.ts";
7
7
  import { changeBetween, externalUpdate } from "./extensions.ts";
8
8
  import "./markdown-editor.css";
9
+ import "./code-token-colors.css";
9
10
 
10
11
  export interface CodeEditorProps {
11
12
  value: string;
@@ -0,0 +1,39 @@
1
+ /* Generated by build-code-theme.ts from the palette in src/palette.ts. DO NOT EDIT. */
2
+ /* The code tokens of the editors, in fences and in the frontmatter's yaml. */
3
+
4
+ .markdown-editor {
5
+ --md-tok-comment: #63707e;
6
+ --md-tok-keyword: #cd112a;
7
+ --md-tok-string: #064d8e;
8
+ --md-tok-constant: #0c71c1;
9
+ --md-tok-property: #0c71c1;
10
+ --md-tok-type: #986b0c;
11
+ --md-tok-function: #8837df;
12
+ --md-tok-invalid: #cd112a;
13
+ }
14
+
15
+ .dark .markdown-editor {
16
+ --md-tok-comment: #b4bdc7;
17
+ --md-tok-keyword: #f56d68;
18
+ --md-tok-string: #97caf6;
19
+ --md-tok-constant: #47a5f4;
20
+ --md-tok-property: #47a5f4;
21
+ --md-tok-type: #e2b83c;
22
+ --md-tok-function: #af87e8;
23
+ --md-tok-invalid: #f56d68;
24
+ }
25
+
26
+ /* the "system" preference never sets .dark: the palette resolves it with this
27
+ media query, so the dark tokens are repeated here */
28
+ @media (prefers-color-scheme: dark) {
29
+ .system .markdown-editor {
30
+ --md-tok-comment: #b4bdc7;
31
+ --md-tok-keyword: #f56d68;
32
+ --md-tok-string: #97caf6;
33
+ --md-tok-constant: #47a5f4;
34
+ --md-tok-property: #47a5f4;
35
+ --md-tok-type: #e2b83c;
36
+ --md-tok-function: #af87e8;
37
+ --md-tok-invalid: #f56d68;
38
+ }
39
+ }
@@ -13,7 +13,8 @@ import { tags } from "@lezer/highlight";
13
13
  * **Every style is scoped to a nested language**: `@lezer/markdown` tags its
14
14
  * own nodes too (`heading1`, `strong`), and an unscoped style would paint prose
15
15
  * a second time over the `md-*` classes. Classes, not colours: the palette is
16
- * in `markdown-editor.css`, switched with the theme.
16
+ * in `code-token-colors.css`, generated with the diff's reviewflow theme
17
+ * (`build-code-theme.ts`), switched with the theme.
17
18
  *
18
19
  * Coarse on purpose — a parent tag catches its children, so a language emitting
19
20
  * a tag nobody listed still lands somewhere sensible. Ported from
@@ -28,14 +28,7 @@
28
28
  --md-frontmatter-bg: color-mix(in srgb, var(--color-muted) 7%, transparent);
29
29
  --md-quote-bg: color-mix(in srgb, var(--color-muted) 6%, transparent);
30
30
 
31
- --md-tok-comment: #6e7781;
32
- --md-tok-keyword: #cf222e;
33
- --md-tok-string: #0a3069;
34
- --md-tok-constant: #0550ae;
35
- --md-tok-property: #0550ae;
36
- --md-tok-type: #116329;
37
- --md-tok-function: #8250df;
38
- --md-tok-invalid: #82071e;
31
+ /* the --md-tok-* colors are generated, in code-token-colors.css */
39
32
 
40
33
  /* fills a column the caller grew; the editor still sizes to its content, and
41
34
  the page — the app layout is the scroll container — scrolls a long one */
@@ -53,32 +46,6 @@
53
46
  transition: border-color 150ms ease-in;
54
47
  }
55
48
 
56
- .dark .markdown-editor {
57
- --md-tok-comment: #8b949e;
58
- --md-tok-keyword: #ff7b72;
59
- --md-tok-string: #a5d6ff;
60
- --md-tok-constant: #79c0ff;
61
- --md-tok-property: #79c0ff;
62
- --md-tok-type: #7ee787;
63
- --md-tok-function: #d2a8ff;
64
- --md-tok-invalid: #f85149;
65
- }
66
-
67
- /* the "system" preference never sets .dark: the palette resolves it with this
68
- media query, so the dark tokens are repeated here */
69
- @media (prefers-color-scheme: dark) {
70
- .system .markdown-editor {
71
- --md-tok-comment: #8b949e;
72
- --md-tok-keyword: #ff7b72;
73
- --md-tok-string: #a5d6ff;
74
- --md-tok-constant: #79c0ff;
75
- --md-tok-property: #79c0ff;
76
- --md-tok-type: #7ee787;
77
- --md-tok-function: #d2a8ff;
78
- --md-tok-invalid: #f85149;
79
- }
80
- }
81
-
82
49
  .markdown-editor:hover {
83
50
  border-color: var(--color-interactive-outlined-hover);
84
51
  }