@reviewflow/ui 6.23.0 → 6.24.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reviewflow/ui",
3
- "version": "6.23.0",
3
+ "version": "6.24.0",
4
4
  "description": "react-native pull request screen components. Consumed as SOURCES (no build): exports resolve `default` to ./src/index.ts, which metro transforms like the app's own files; the consuming app's css must @source this package's src so tailwind keeps its classes.",
5
5
  "keywords": [
6
6
  "alouette",
@@ -52,7 +52,7 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@codemirror/autocomplete": "6.20.3",
55
+ "@codemirror/autocomplete": "6.20.4",
56
56
  "@codemirror/commands": "6.11.1",
57
57
  "@codemirror/lang-css": "6.3.1",
58
58
  "@codemirror/lang-html": "6.4.12",
@@ -60,15 +60,15 @@
60
60
  "@codemirror/lang-json": "6.0.2",
61
61
  "@codemirror/lang-markdown": "6.5.2",
62
62
  "@codemirror/lang-yaml": "6.1.3",
63
- "@codemirror/language": "6.12.4",
63
+ "@codemirror/language": "6.13.1",
64
64
  "@codemirror/search": "6.7.2",
65
- "@codemirror/state": "6.7.5",
66
- "@codemirror/view": "6.43.12",
65
+ "@codemirror/state": "6.7.6",
66
+ "@codemirror/view": "6.43.15",
67
67
  "@lezer/common": "1.5.5",
68
68
  "@lezer/highlight": "1.2.3",
69
69
  "@lezer/markdown": "1.7.2",
70
70
  "@pierre/diffs": "1.5.2",
71
- "@reviewflow/sdk": "6.23.0",
71
+ "@reviewflow/sdk": "6.24.0",
72
72
  "@tanstack/markdown": "0.0.13",
73
73
  "alouette-icons": "12.0.0",
74
74
  "gemoji": "8.1.0",
@@ -53,7 +53,7 @@ const submitBlocker = (
53
53
  };
54
54
 
55
55
  /**
56
- * "Review changes", ending the Files changed tab's sticky toolbar: the verdict, a
56
+ * "Review changes", ending the Review tab's sticky toolbar: the verdict, a
57
57
  * summary and the inline comments the diff collected, sent as one review.
58
58
  * Brand while comments are pending, so an unsent review is not forgotten. Never
59
59
  * offered to the author, whom GitHub lets comment but not review. A review
@@ -181,7 +181,7 @@ export function PullRequestReviewButton({
181
181
  </Text>
182
182
  {comments.length === 0 ? (
183
183
  <Text className="text-sm text-muted">
184
- None. Add them from the Files changed tab.
184
+ None. Add them from the diff lines.
185
185
  </Text>
186
186
  ) : (
187
187
  comments.map((comment) => (
@@ -3,6 +3,7 @@ import type {
3
3
  PullRequestDetailResponse,
4
4
  PullRequestEdit,
5
5
  } from "@reviewflow/sdk";
6
+ import { pluralOf } from "@reviewflow/sdk";
6
7
  import {
7
8
  Button,
8
9
  Code,
@@ -213,7 +214,7 @@ export function PullRequestCommits({
213
214
  ))}
214
215
  {unlisted > 0 ? (
215
216
  <Text className="text-xs text-muted">
216
- {`${String(unlisted)} more commit${unlisted === 1 ? "" : "s"} not listed`}
217
+ {`${pluralOf(unlisted, "more commit")} not listed`}
217
218
  </Text>
218
219
  ) : null}
219
220
  </View>
@@ -4,7 +4,7 @@ import type {
4
4
  PullRequestEdit,
5
5
  PullRequestThread,
6
6
  } from "@reviewflow/sdk";
7
- import { suggestionOriginalLines } from "@reviewflow/sdk";
7
+ import { pluralOf, suggestionOriginalLines } from "@reviewflow/sdk";
8
8
  import {
9
9
  ActionButton,
10
10
  Badge,
@@ -114,7 +114,7 @@ export function ThreadAnnotation({
114
114
  Resolved
115
115
  </Badge>
116
116
  <Text className="text-sm text-muted">
117
- {`${first?.author.login ?? "thread"}, ${String(thread.commentsTotal)} comment${thread.commentsTotal === 1 ? "" : "s"}`}
117
+ {`${first?.author.login ?? "thread"}, ${pluralOf(thread.commentsTotal, "comment")}`}
118
118
  </Text>
119
119
  <Button
120
120
  size="sm"
@@ -160,7 +160,7 @@ export function ThreadAnnotation({
160
160
  </SuggestionOriginalContext>
161
161
  {unlisted > 0 ? (
162
162
  <Text className="text-xs text-muted">
163
- {`${String(unlisted)} more comment${unlisted === 1 ? "" : "s"} on GitHub`}
163
+ {`${pluralOf(unlisted, "more comment")} on GitHub`}
164
164
  </Text>
165
165
  ) : null}
166
166
  {thread.viewerCanReply && first !== undefined ? (
@@ -1,10 +1,11 @@
1
1
  import type { DiffImpactsResponse } from "@reviewflow/sdk";
2
+ import { pluralOf } from "@reviewflow/sdk";
2
3
  import { AccentScope, Icon, Text, View } from "alouette";
3
4
  import { InfoRegularIcon } from "alouette-icons/phosphor-icons/Info";
4
5
  import { ShareNetworkDuotoneIcon } from "alouette-icons/phosphor-icons/ShareNetwork";
5
6
  import type { ReactNode } from "react";
6
7
  import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
7
- import { countImpacts, pluralOf } from "./pull-request-diff-impacts.ts";
8
+ import { countImpacts } from "./pull-request-diff-impacts.ts";
8
9
 
9
10
  interface PullRequestDiffImpactsSummaryProps {
10
11
  /** null until read, or when it could not be */
@@ -1,4 +1,5 @@
1
1
  import type { DiffFileImpacts } from "@reviewflow/sdk";
2
+ import { pluralOf } from "@reviewflow/sdk";
2
3
 
3
4
  /** `none`: no checkout, or a source that cannot read impacts. */
4
5
  export type ImpactsStatus = "failed" | "none" | "reading" | "ready";
@@ -34,9 +35,6 @@ export const countImpacts = (
34
35
  /** Past this many files using a changed file's exports, each export carries its counts only. */
35
36
  export const maxImporterNames = 50;
36
37
 
37
- export const pluralOf = (count: number, word: string): string =>
38
- `${String(count)} ${word}${count === 1 ? "" : "s"}`;
39
-
40
38
  /** "4 files · 2 tests", either part left out when it is 0. */
41
39
  export const usageLabel = ({ files, tests }: ImpactCounts): string =>
42
40
  [
@@ -4,7 +4,7 @@ import type {
4
4
  DiffOmittedFile,
5
5
  DiffReadingOrderResponse,
6
6
  } from "@reviewflow/sdk";
7
- import { isAttachablePath, subjectsOf } from "@reviewflow/sdk";
7
+ import { isAttachablePath, pluralOf, subjectsOf } from "@reviewflow/sdk";
8
8
  import { Icon, PressableBox, Text, View } from "alouette";
9
9
  import { FileRegularIcon } from "alouette-icons/phosphor-icons/File";
10
10
  import { FileTsRegularIcon } from "alouette-icons/phosphor-icons/FileTs";
@@ -13,7 +13,6 @@ import { TestTubeRegularIcon } from "alouette-icons/phosphor-icons/TestTube";
13
13
  import type { ReactNode } from "react";
14
14
  import { PullRequestDiffImpactsSummary } from "./pull-request-diff-impacts-summary.tsx";
15
15
  import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
16
- import { pluralOf } from "./pull-request-diff-impacts.ts";
17
16
 
18
17
  /** Past this many names over the range, the rows carry their counts only. */
19
18
  export const maxOverviewNames = 50;
@@ -272,7 +272,7 @@ export function PullRequestDiffView({
272
272
  <View
273
273
  className={
274
274
  aside && diffStyle === "split"
275
- ? "gap-m web:relative web:left-1/2 web:w-[min(100vw-3rem,1800px)] web:-translate-x-1/2"
275
+ ? "gap-m web:relative web:left-1/2 web:w-[100cqw] web:-translate-x-1/2"
276
276
  : "gap-m"
277
277
  }
278
278
  >
@@ -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,
@@ -70,7 +71,6 @@ import type { PullRequestMovedOn } from "./pull-request-diff-data.ts";
70
71
  import {
71
72
  countImpacts,
72
73
  maxImporterNames,
73
- pluralOf,
74
74
  usageLabel,
75
75
  } from "./pull-request-diff-impacts.ts";
76
76
  import type { DiffFileHighlight } from "./pull-request-diff-jump.ts";
@@ -214,8 +214,7 @@ const useStableAnnotations = (
214
214
  /** Why a thread is listed above its file rather than placed on a line. */
215
215
  type UnplacedKind = "changedSince" | "outdated" | "whitespace";
216
216
 
217
- const commentsText = (count: number): string =>
218
- `${String(count)} comment${count === 1 ? "" : "s"}`;
217
+ const commentsText = (count: number): string => pluralOf(count, "comment");
219
218
 
220
219
  const unplacedLabel = (kind: UnplacedKind, count: number): string => {
221
220
  if (kind === "changedSince") {
@@ -224,7 +223,7 @@ const unplacedLabel = (kind: UnplacedKind, count: number): string => {
224
223
  if (kind === "whitespace") {
225
224
  return `${commentsText(count)} on lines hidden with whitespace changes`;
226
225
  }
227
- return `${String(count)} outdated comment${count === 1 ? "" : "s"}`;
226
+ return pluralOf(count, "outdated comment");
228
227
  };
229
228
 
230
229
  const unplacedLineText = (
@@ -6,7 +6,7 @@ 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,
@@ -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(" · ");
@@ -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. */
@@ -1174,7 +1173,7 @@ export interface PullRequestPanelProps extends CandidatesProps {
1174
1173
  /**
1175
1174
  * One pull request as GitHub reports it now, laid out as GitHub's own page —
1176
1175
  * the header, then Timeline (the description, a status box and a narrow
1177
- * sidebar), Checks and Files changed — with the edits the operator makes
1176
+ * sidebar), Commits, Checks and Review — with the edits the operator makes
1178
1177
  * before asking for a review. Every edit is GitHub's to refuse, and the panel
1179
1178
  * re-reads the pull request after it, so nothing on screen is the request
1180
1179
  * echoed back.
@@ -1194,6 +1193,13 @@ export function PullRequestPanel({
1194
1193
  pullRequest,
1195
1194
  "checks",
1196
1195
  ).value;
1196
+ const viewerIsAuthor = isViewerAuthor(detail);
1197
+ const reviewTab = (
1198
+ <Tab
1199
+ value="review"
1200
+ label={`Review (${pluralOf(pullRequest.changedFiles, "file")})`}
1201
+ />
1202
+ );
1197
1203
  return (
1198
1204
  <View className="gap-l">
1199
1205
  {[...new Set([...errors, ...(candidates?.errors ?? [])])].map((error) => (
@@ -1214,6 +1220,7 @@ export function PullRequestPanel({
1214
1220
  if (isPullRequestTab(next)) onTabChange(next);
1215
1221
  }}
1216
1222
  >
1223
+ {viewerIsAuthor ? null : reviewTab}
1217
1224
  <Tab value="timeline" label="Conversation" />
1218
1225
  <Tab
1219
1226
  value="commits"
@@ -1227,11 +1234,7 @@ export function PullRequestPanel({
1227
1234
  : `Checks (${String(checks.checksTotal)})`
1228
1235
  }
1229
1236
  />
1230
- <Tab
1231
- value="files"
1232
- label={`Files changed (${String(pullRequest.changedFiles)})`}
1233
- />
1234
- <Tab value="review" label="Review" />
1237
+ {viewerIsAuthor ? reviewTab : null}
1235
1238
  </Tabs>
1236
1239
  <PullRequestActions detail={detail} onEdited={onEdited} />
1237
1240
  </View>
@@ -1247,9 +1250,6 @@ export function PullRequestPanel({
1247
1250
  );
1248
1251
  }
1249
1252
  if (tab === "checks") return <ChecksTab pullRequest={pullRequest} />;
1250
- if (tab === "files") {
1251
- return <PullRequestFiles detail={detail} onEdited={onEdited} />;
1252
- }
1253
1253
  if (tab === "review") {
1254
1254
  return <PullRequestReview detail={detail} onEdited={onEdited} />;
1255
1255
  }
@@ -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
  }
@@ -253,11 +253,17 @@ export interface PullRequestScreenProps {
253
253
 
254
254
  /**
255
255
  * 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
256
+ * Conversation, Commits, Checks and Review, with the edits a reviewer
257
257
  * or an author makes. Reads and writes through the
258
258
  * {@link PullRequestDataSource} of the nearest
259
259
  * `PullRequestDataSourceProvider`, which the app provides. Mount it with a
260
260
  * `key` of the pull request, so a draft review never leaks to the next one.
261
+ *
262
+ * It fills its parent, and the app chooses how wide that is: its text stays in
263
+ * a 1200px column, and only a split diff beside its file list widens to the
264
+ * whole screen. The screen is the CSS container that diff is sized against
265
+ * (`100cqw`): the window (`100vw`) overflows an app that lays the page beside a
266
+ * sidebar.
261
267
  */
262
268
  export function PullRequestScreen({
263
269
  ownerRepo,
@@ -280,18 +286,20 @@ export function PullRequestScreen({
280
286
  fileOrder={diffFileOrder}
281
287
  onFileOrderChange={onDiffFileOrderChange}
282
288
  >
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>
289
+ <View className="w-full px-l pt-l pb-xl web:@container">
290
+ <View className="mx-auto w-full max-w-[1200px] gap-m">
291
+ <PullRequestDraftsProvider
292
+ store={draftStore}
293
+ pullRequest={{ ownerRepo, number }}
294
+ >
295
+ <PullRequestBody
296
+ key={`${ownerRepo}#${String(number)}`}
297
+ ownerRepo={ownerRepo}
298
+ number={number}
299
+ {...body}
300
+ />
301
+ </PullRequestDraftsProvider>
302
+ </View>
295
303
  </View>
296
304
  </DiffSettingsProvider>
297
305
  </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({