@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.
- package/package.json +21 -6
- package/src/components/branch-diff/diff-font.ts +26 -0
- package/src/components/branch-diff/diff-themes.ts +53 -1
- package/src/components/branch-diff/reviewflow-diff-selection.ts +11 -0
- package/src/components/branch-diff/reviewflow-diff-themes.ts +2218 -0
- package/src/components/branch-diff/use-diff-settings.tsx +15 -3
- package/src/components/branch-diff/whitespace-button.tsx +2 -2
- package/src/components/diff-settings-section.tsx +189 -0
- package/src/components/diff-theme-preview.tsx +63 -0
- package/src/components/file-kind.ts +91 -0
- package/src/components/file-type-icon.tsx +70 -0
- package/src/components/pull-request-actions-review.tsx +2 -2
- package/src/components/pull-request-commits.tsx +2 -1
- package/src/components/pull-request-diff-annotations.tsx +8 -5
- package/src/components/pull-request-diff-impacts-summary.tsx +2 -1
- package/src/components/pull-request-diff-impacts.ts +1 -3
- package/src/components/pull-request-diff-jump.ts +59 -1
- package/src/components/pull-request-diff-overview-tree.ts +57 -0
- package/src/components/pull-request-diff-overview.tsx +489 -90
- package/src/components/pull-request-diff-tree-files.ts +64 -0
- package/src/components/pull-request-diff-tree.tsx +232 -0
- package/src/components/pull-request-diff-view.tsx +112 -28
- package/src/components/pull-request-diff-viewer.tsx +26 -6
- package/src/components/pull-request-files.tsx +6 -6
- package/src/components/pull-request-panel.tsx +23 -21
- package/src/components/pull-request-reactions.tsx +2 -2
- package/src/components/pull-request-revisions.tsx +2 -1
- package/src/components/pull-request-screen.tsx +37 -17
- package/src/components/pull-request-suggestion-actions.tsx +7 -6
- package/src/components/repository-attachment-banner.tsx +1 -1
- package/src/components/scroll-parent.ts +20 -0
- package/src/components/tooltip-icon-button.tsx +27 -0
- package/src/editor/code-editor.tsx +1 -0
- package/src/editor/code-token-colors.css +39 -0
- package/src/editor/codeHighlight.ts +2 -1
- package/src/editor/markdown-editor.css +1 -34
- package/src/editor/markdown-editor.tsx +1 -0
- package/src/index.ts +6 -6
- 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 {
|
|
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
|
|
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
|
-
<
|
|
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 =
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
{`⋯ ${
|
|
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
|
|
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
|
-
/**
|
|
207
|
-
|
|
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 `
|
|
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
|
|
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="
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 ${
|
|
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
|
|
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 `
|
|
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-
|
|
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
|
}
|