@reviewflow/ui 6.22.2 → 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.22.2",
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.22.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",
@@ -1,4 +1,16 @@
1
- import type { PullRequestRef, ReviewflowBoardPr } from "@reviewflow/sdk";
1
+ import {
2
+ formatPrChanges,
3
+ reviewflowPrChangesRequested,
4
+ reviewflowPrFailure,
5
+ reviewflowPrFlowDate,
6
+ reviewflowPrOwners,
7
+ reviewflowPrProgress,
8
+ } from "@reviewflow/sdk";
9
+ import type {
10
+ PullRequestRef,
11
+ ReviewflowBoardPr,
12
+ ReviewflowRowContext,
13
+ } from "@reviewflow/sdk";
2
14
  import {
3
15
  AccentScope,
4
16
  ExternalLinkText,
@@ -11,15 +23,6 @@ import type { ReactNode } from "react";
11
23
  import type { GestureResponderEvent } from "react-native";
12
24
  import { linkInRow } from "../components/link-in-row.ts";
13
25
  import { isSafeUrl } from "../markdown-github/safe-url.ts";
14
- import {
15
- formatPrChanges,
16
- reviewflowPrChangesRequested,
17
- reviewflowPrFailure,
18
- reviewflowPrFlowDate,
19
- reviewflowPrOwners,
20
- reviewflowPrProgress,
21
- } from "./facts.ts";
22
- import type { ReviewflowRowContext } from "./facts.ts";
23
26
 
24
27
  /**
25
28
  * Following a link from inside the pressable row must not also press the row.
@@ -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,6 +6,7 @@ import type {
6
6
  PullRequestThread,
7
7
  PullRequestViewerLastReview,
8
8
  } from "@reviewflow/sdk";
9
+ import { formatRelativeDate, pluralOf } from "@reviewflow/sdk";
9
10
  import {
10
11
  Button,
11
12
  Code,
@@ -27,7 +28,6 @@ import { useMemo, useState } from "react";
27
28
  import { usePullRequestDataSource } from "../dataSource.ts";
28
29
  import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
29
30
  import { errorToMessage } from "../services/errorToMessage.ts";
30
- import { formatRelativeDate } from "../services/formatRelativeDate.ts";
31
31
  import { isViewerAuthor } from "../services/isViewerAuthor.ts";
32
32
  import { PullRequestReviewButton } from "./pull-request-actions-review.tsx";
33
33
  import type {
@@ -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(" · ");
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ PrStatus,
2
3
  PullRequestCandidatesResponse,
3
4
  PullRequestDetail,
4
5
  PullRequestDetailResponse,
@@ -6,6 +7,12 @@ import type {
6
7
  PullRequestReviewState,
7
8
  PullRequestSummary,
8
9
  } from "@reviewflow/sdk";
10
+ import {
11
+ pluralOf,
12
+ prStatus,
13
+ prStatusLabels,
14
+ reviewflowPrStatus,
15
+ } from "@reviewflow/sdk";
9
16
  import type { Accent } from "alouette";
10
17
  import {
11
18
  AccentScope,
@@ -33,16 +40,11 @@ import { ChatCircleDotsRegularIcon } from "alouette-icons/phosphor-icons/ChatCir
33
40
  import type { ReactNode } from "react";
34
41
  import { useState } from "react";
35
42
  import { usePullRequestDataSource } from "../dataSource.ts";
36
- import type { PrStatus } from "../prStatus.ts";
37
- import {
38
- prStatus,
39
- prStatusAccents,
40
- prStatusLabels,
41
- reviewflowPrStatus,
42
- } from "../prStatus.ts";
43
+ import { prStatusAccents } from "../prStatus.ts";
43
44
  import { emojify } from "../services/emojify.ts";
44
45
  import { errorToMessage } from "../services/errorToMessage.ts";
45
46
  import { formatDataAge } from "../services/formatDataAge.ts";
47
+ import { isViewerAuthor } from "../services/isViewerAuthor.ts";
46
48
  import { LabelPill } from "./label-pill.tsx";
47
49
  import {
48
50
  PullRequestMergeButton,
@@ -52,7 +54,6 @@ import {
52
54
  import { CheckLine } from "./pull-request-checks.tsx";
53
55
  import { PullRequestCommits } from "./pull-request-commits.tsx";
54
56
  import { PersonAvatar } from "./pull-request-conversation-person.tsx";
55
- import { PullRequestFiles } from "./pull-request-files.tsx";
56
57
  import {
57
58
  PullRequestPartError,
58
59
  PullRequestPartPending,
@@ -365,7 +366,7 @@ function UnresolvedThreads({
365
366
  </View>
366
367
  </AccentScope>
367
368
  <Text className="min-w-0 flex-1 text-sm">
368
- {`${String(unresolved)} unresolved ${unresolved === 1 ? "thread" : "threads"}`}
369
+ {pluralOf(unresolved, "unresolved thread")}
369
370
  </Text>
370
371
  </View>
371
372
  );
@@ -1124,18 +1125,12 @@ function Sidebar({ detail, candidates, onEdited }: CandidatesProps): ReactNode {
1124
1125
  );
1125
1126
  }
1126
1127
 
1127
- export type PullRequestTab =
1128
- | "checks"
1129
- | "commits"
1130
- | "files"
1131
- | "review"
1132
- | "timeline";
1128
+ export type PullRequestTab = "checks" | "commits" | "review" | "timeline";
1133
1129
 
1134
1130
  export const isPullRequestTab = (value: unknown): value is PullRequestTab =>
1135
1131
  value === "checks" ||
1136
1132
  value === "commits" ||
1137
1133
  value === "timeline" ||
1138
- value === "files" ||
1139
1134
  value === "review";
1140
1135
 
1141
1136
  /** The timeline opens on the description, as GitHub's first comment. */
@@ -1178,7 +1173,7 @@ export interface PullRequestPanelProps extends CandidatesProps {
1178
1173
  /**
1179
1174
  * One pull request as GitHub reports it now, laid out as GitHub's own page —
1180
1175
  * the header, then Timeline (the description, a status box and a narrow
1181
- * sidebar), Checks and Files changed — with the edits the operator makes
1176
+ * sidebar), Commits, Checks and Review — with the edits the operator makes
1182
1177
  * before asking for a review. Every edit is GitHub's to refuse, and the panel
1183
1178
  * re-reads the pull request after it, so nothing on screen is the request
1184
1179
  * echoed back.
@@ -1198,6 +1193,13 @@ export function PullRequestPanel({
1198
1193
  pullRequest,
1199
1194
  "checks",
1200
1195
  ).value;
1196
+ const viewerIsAuthor = isViewerAuthor(detail);
1197
+ const reviewTab = (
1198
+ <Tab
1199
+ value="review"
1200
+ label={`Review (${pluralOf(pullRequest.changedFiles, "file")})`}
1201
+ />
1202
+ );
1201
1203
  return (
1202
1204
  <View className="gap-l">
1203
1205
  {[...new Set([...errors, ...(candidates?.errors ?? [])])].map((error) => (
@@ -1218,6 +1220,7 @@ export function PullRequestPanel({
1218
1220
  if (isPullRequestTab(next)) onTabChange(next);
1219
1221
  }}
1220
1222
  >
1223
+ {viewerIsAuthor ? null : reviewTab}
1221
1224
  <Tab value="timeline" label="Conversation" />
1222
1225
  <Tab
1223
1226
  value="commits"
@@ -1231,11 +1234,7 @@ export function PullRequestPanel({
1231
1234
  : `Checks (${String(checks.checksTotal)})`
1232
1235
  }
1233
1236
  />
1234
- <Tab
1235
- value="files"
1236
- label={`Files changed (${String(pullRequest.changedFiles)})`}
1237
- />
1238
- <Tab value="review" label="Review" />
1237
+ {viewerIsAuthor ? reviewTab : null}
1239
1238
  </Tabs>
1240
1239
  <PullRequestActions detail={detail} onEdited={onEdited} />
1241
1240
  </View>
@@ -1251,9 +1250,6 @@ export function PullRequestPanel({
1251
1250
  );
1252
1251
  }
1253
1252
  if (tab === "checks") return <ChecksTab pullRequest={pullRequest} />;
1254
- if (tab === "files") {
1255
- return <PullRequestFiles detail={detail} onEdited={onEdited} />;
1256
- }
1257
1253
  if (tab === "review") {
1258
1254
  return <PullRequestReview detail={detail} onEdited={onEdited} />;
1259
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({
@@ -2,7 +2,8 @@
2
2
  * The markdown editor's look, ported from vscode-extension-markdown-beautiful-editor
3
3
  * (src/styles/md-inline.css, md-block.css, md-alerts.css, code-tokens.css) with
4
4
  * every --vscode-* colour rewritten as an alouette token, so the light and dark
5
- * themes reach it through the same .light/.dark ancestor as the rest of the app.
5
+ * themes reach it through the same .light/.dark/.system ancestor as the rest of
6
+ * the app.
6
7
  *
7
8
  * Everything is scoped under .markdown-editor: the md-* classes are generic
8
9
  * names and must not reach anything else on the page.
@@ -63,6 +64,21 @@
63
64
  --md-tok-invalid: #f85149;
64
65
  }
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
+
66
82
  .markdown-editor:hover {
67
83
  border-color: var(--color-interactive-outlined-hover);
68
84
  }
package/src/index.ts CHANGED
@@ -59,8 +59,7 @@ export { PullRequestSuggestionBatchProvider } from "./services/PullRequestSugges
59
59
  export { emojify } from "./services/emojify.ts";
60
60
  export { errorToMessage } from "./services/errorToMessage.ts";
61
61
  export { isViewerAuthor } from "./services/isViewerAuthor.ts";
62
- export type { PrStatus } from "./prStatus.ts";
63
- export { prStatus, prStatusAccents, prStatusLabels } from "./prStatus.ts";
62
+ export { prStatusAccents } from "./prStatus.ts";
64
63
  export * from "./board/index.ts";
65
64
  export { MarkdownEditor } from "./editor/markdown-editor.tsx";
66
65
  export type { MarkdownEditorFieldProps } from "./editor/markdown-editor.tsx";
package/src/prStatus.ts CHANGED
@@ -1,62 +1,6 @@
1
- import type {
2
- PrReviewDecision,
3
- PrState,
4
- ReviewflowBoardPr,
5
- } from "@reviewflow/sdk";
1
+ import type { PrStatus } from "@reviewflow/sdk";
6
2
  import type { Accent } from "alouette";
7
3
 
8
- /**
9
- * What a reader is told about a pull request in one word or two: an open pull
10
- * request is only interesting for where its review stands, so state and review
11
- * decision collapse into one status.
12
- */
13
- export type PrStatus =
14
- | "approved"
15
- | "changesRequested"
16
- | "closed"
17
- | "draft"
18
- | "merged"
19
- | "noReviewers"
20
- | "waitingReview";
21
-
22
- /** A closed or merged pull request keeps its own status whatever the last review said. */
23
- export const prStatus = ({
24
- state,
25
- reviewDecision,
26
- }: {
27
- state: PrState;
28
- reviewDecision: PrReviewDecision;
29
- }): PrStatus => {
30
- if (state !== "open") return state;
31
- switch (reviewDecision) {
32
- case "approved": {
33
- return "approved";
34
- }
35
- case "changesRequested": {
36
- return "changesRequested";
37
- }
38
- case "reviewRequired": {
39
- return "waitingReview";
40
- }
41
- case null: {
42
- return "noReviewers";
43
- }
44
- default: {
45
- return reviewDecision satisfies never;
46
- }
47
- }
48
- };
49
-
50
- export const prStatusLabels: Record<PrStatus, string> = {
51
- approved: "approved",
52
- changesRequested: "changes requested",
53
- closed: "closed",
54
- draft: "draft",
55
- merged: "merged",
56
- noReviewers: "no reviewers",
57
- waitingReview: "waiting review",
58
- };
59
-
60
4
  /** A merged pull request takes the brand accent; an app with its own accent for it passes one instead. */
61
5
  export const prStatusAccents: Record<PrStatus, Accent | undefined> = {
62
6
  approved: "success",
@@ -67,24 +11,3 @@ export const prStatusAccents: Record<PrStatus, Accent | undefined> = {
67
11
  noReviewers: "warning",
68
12
  waitingReview: "info",
69
13
  };
70
-
71
- const reviewflowReviewDecisions = {
72
- approved: "approved",
73
- changesRequested: "changesRequested",
74
- requested: "reviewRequired",
75
- none: null,
76
- } as const;
77
-
78
- /** null when reviewflow does not know enough to say */
79
- export const reviewflowPrStatus = ({
80
- state,
81
- reviewState,
82
- }: Pick<ReviewflowBoardPr, "reviewState" | "state">): PrStatus | null => {
83
- if (state === null) return null;
84
- if (state !== "open") return state;
85
- if (reviewState === null) return null;
86
- return prStatus({
87
- state,
88
- reviewDecision: reviewflowReviewDecisions[reviewState],
89
- });
90
- };
@@ -1,183 +0,0 @@
1
- import type { ReviewflowBoardPr, ReviewflowPrChanges } from "@reviewflow/sdk";
2
- import type { FormatRelativeDateOptions } from "../services/formatRelativeDate.ts";
3
- import { formatRelativeDate } from "../services/formatRelativeDate.ts";
4
-
5
- /**
6
- * What reviewflow's own board row says about a pull request, in its wording so
7
- * the two boards read the same. The status line comes in three parts by how
8
- * loudly each is shown: what blocks the merge, changes requested, the rest.
9
- */
10
-
11
- export const maxFailedCheckNames = 9;
12
-
13
- /** Where a row is shown, which decides what it leaves to its bucket's title. */
14
- export interface ReviewflowRowContext {
15
- bucketKey: string;
16
- groupKey: string;
17
- /** the token's login, named "you" */
18
- viewer: string | null;
19
- }
20
-
21
- /** the buckets whose title already says whether the checks pass */
22
- const bucketsSilentOnPassedChecks = new Set(["changes-requested", "drafts"]);
23
- /** the bucket whose title already says every reviewer was asked again */
24
- const reRequestedBucket = "re-requested-reviews";
25
- /** someone else's pull request requests a review, the operator's awaits one */
26
- const requestingAttentionGroup = "requesting-attention";
27
-
28
- const counted = (count: number, one: string, many: string): string =>
29
- `${String(count)} ${count === 1 ? one : many}`;
30
-
31
- const people = (logins: string[], viewer: string | null): string =>
32
- logins.map((login) => (login === viewer ? "you" : `@${login}`)).join(", ");
33
-
34
- const failedChecksText = ({
35
- checksConclusion,
36
- failedChecks,
37
- }: Pick<ReviewflowBoardPr, "checksConclusion" | "failedChecks">):
38
- | string
39
- | null => {
40
- if (failedChecks.length === 0) {
41
- return checksConclusion === "failed" ? "checks failed" : null;
42
- }
43
- const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
44
- const remaining = failedChecks.length - maxFailedCheckNames;
45
- const more = remaining > 0 ? ` +${String(remaining)}` : "";
46
- return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
47
- };
48
-
49
- /**
50
- * Failing checks (after reviewflow's allowed-to-fail rules, by name), a failed
51
- * pr lint and a merge conflict; null when nothing blocks.
52
- */
53
- export const reviewflowPrFailure = (
54
- pr: Pick<
55
- ReviewflowBoardPr,
56
- "checksConclusion" | "failedChecks" | "lintFailed" | "mergeable"
57
- >,
58
- ): string | null => {
59
- const parts = [
60
- failedChecksText(pr),
61
- pr.lintFailed ? "pr lint failed" : null,
62
- pr.mergeable === "conflicting" ? "conflicts" : null,
63
- ].filter((part) => part !== null);
64
- return parts.length === 0 ? null : parts.join(" · ");
65
- };
66
-
67
- export const reviewflowPrChangesRequested = (
68
- { changesRequestedBy }: Pick<ReviewflowBoardPr, "changesRequestedBy">,
69
- viewer: string | null,
70
- ): string | null =>
71
- changesRequestedBy.length === 0
72
- ? null
73
- : `changes requested by ${people(changesRequestedBy, viewer)}`;
74
-
75
- const checksProgressText = (
76
- {
77
- checksConclusion,
78
- checksRunning,
79
- }: Pick<ReviewflowBoardPr, "checksConclusion" | "checksRunning">,
80
- bucketKey: string,
81
- ): string | null => {
82
- if (checksRunning > 0) {
83
- return `${counted(checksRunning, "check", "checks")} running`;
84
- }
85
- return checksConclusion === "passed" &&
86
- !bucketsSilentOnPassedChecks.has(bucketKey)
87
- ? "checks passed"
88
- : null;
89
- };
90
-
91
- const awaitedText = (
92
- {
93
- requestedReviewers,
94
- reRequestedReviewers,
95
- requestedTeams,
96
- }: Pick<
97
- ReviewflowBoardPr,
98
- "requestedReviewers" | "requestedTeams" | "reRequestedReviewers"
99
- >,
100
- { bucketKey, groupKey, viewer }: ReviewflowRowContext,
101
- ): string | null => {
102
- const reviewers = [
103
- ...requestedReviewers,
104
- ...(bucketKey === reRequestedBucket ? [] : reRequestedReviewers),
105
- ];
106
- if (reviewers.length === 0 && requestedTeams.length === 0) return null;
107
- const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
108
- const awaited = [
109
- ...(reviewers.length === 0 ? [] : [people(reviewers, viewer)]),
110
- ...requestedTeams.map((team) => `#${team}`),
111
- ];
112
- return `${verb} ${awaited.join(", ")}`;
113
- };
114
-
115
- /** running or passed checks, approvals, open threads, awaited reviewers */
116
- export const reviewflowPrProgress = (
117
- pr: Pick<
118
- ReviewflowBoardPr,
119
- | "approvedBy"
120
- | "checksConclusion"
121
- | "checksRunning"
122
- | "requestedReviewers"
123
- | "requestedTeams"
124
- | "reRequestedReviewers"
125
- | "unresolvedThreads"
126
- >,
127
- context: ReviewflowRowContext,
128
- ): string | null => {
129
- const parts = [
130
- checksProgressText(pr, context.bucketKey),
131
- pr.approvedBy.length === 0
132
- ? null
133
- : `approved by ${people(pr.approvedBy, context.viewer)}`,
134
- pr.unresolvedThreads
135
- ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads")
136
- : null,
137
- awaitedText(pr, context),
138
- ].filter((part) => part !== null);
139
- return parts.length === 0 ? null : parts.join(" · ");
140
- };
141
-
142
- /**
143
- * "by @author", "assigned to @a", or both. An author who is their own only
144
- * assignee is a byline, not an assignment; a pull request whose only person is
145
- * the viewer names nobody.
146
- */
147
- export const reviewflowPrOwners = (
148
- { author, assignees }: Pick<ReviewflowBoardPr, "assignees" | "author">,
149
- viewer: string | null,
150
- ): string | null => {
151
- const named = [...(author === null ? [] : [author]), ...assignees];
152
- if (named.length === 0) return null;
153
- if (viewer !== null && named.every((login) => login === viewer)) return null;
154
-
155
- const byline = author === null ? null : `by ${people([author], viewer)}`;
156
- if (assignees.length === 0) return byline;
157
- const assignment = `assigned to ${people(assignees, viewer)}`;
158
-
159
- if (author === null || !assignees.includes(author)) {
160
- return byline === null ? assignment : `${byline} · ${assignment}`;
161
- }
162
- // naming every assignee already names the author among them
163
- return assignees.length === 1 ? byline : assignment;
164
- };
165
-
166
- export const reviewflowPrFlowDate = (
167
- { openedAt, approvedAt }: Pick<ReviewflowBoardPr, "approvedAt" | "openedAt">,
168
- options?: FormatRelativeDateOptions,
169
- ): string | null => {
170
- if (approvedAt !== null) {
171
- return `approved ${formatRelativeDate(approvedAt, options)}`;
172
- }
173
- return openedAt === null
174
- ? null
175
- : `opened ${formatRelativeDate(openedAt, options)}`;
176
- };
177
-
178
- export const formatPrChanges = ({
179
- changedFiles,
180
- additions,
181
- deletions,
182
- }: ReviewflowPrChanges): string =>
183
- `${counted(changedFiles, "file", "files")} +${String(additions)} −${String(deletions)}`;
@@ -1,56 +0,0 @@
1
- /**
2
- * A past date the way the operator says it: "today 14:32", "yesterday 09:10",
3
- * "last Monday 16:05", then "12 Oct, 14:32" — and the year once it is not this
4
- * one. The boards read dates to answer "is this from this morning or last
5
- * week", which a full timestamp makes them work out.
6
- *
7
- * Calendar days in the machine's own time zone, not 24-hour spans: 23:50
8
- * yesterday is "yesterday" at 00:10. The time and the month follow the
9
- * locale; the words are the app's, which is in English.
10
- */
11
-
12
- const dayMs = 24 * 60 * 60 * 1000;
13
-
14
- /** how many days back the weekday alone still names the day without doubt */
15
- const weekdayReach = 6;
16
-
17
- const startOfDay = (date: Date): number =>
18
- new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
19
-
20
- /** Whole calendar days from `date` to `now`; negative for a future date. */
21
- const calendarDaysAgo = (date: Date, now: Date): number =>
22
- Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
23
-
24
- export interface FormatRelativeDateOptions {
25
- now?: Date;
26
- /** a BCP 47 tag; the runtime's own when absent */
27
- locale?: string;
28
- }
29
-
30
- export const formatRelativeDate = (
31
- value: Date | string,
32
- { now = new Date(), locale }: FormatRelativeDateOptions = {},
33
- ): string => {
34
- const date = typeof value === "string" ? new Date(value) : value;
35
- if (Number.isNaN(date.getTime())) return "unknown date";
36
-
37
- const time = date.toLocaleTimeString(locale, {
38
- hour: "2-digit",
39
- minute: "2-digit",
40
- });
41
- const days = calendarDaysAgo(date, now);
42
-
43
- if (days === 0) return `today ${time}`;
44
- if (days === 1) return `yesterday ${time}`;
45
- if (days > 1 && days <= weekdayReach) {
46
- const weekday = date.toLocaleDateString("en", { weekday: "long" });
47
- return `last ${weekday} ${time}`;
48
- }
49
-
50
- const day = date.toLocaleDateString(locale, {
51
- day: "numeric",
52
- month: "short",
53
- ...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
54
- });
55
- return `${day}, ${time}`;
56
- };