@reviewflow/ui 0.0.0-stage → 6.21.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 (92) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +95 -2
  3. package/package.json +102 -4
  4. package/src/board/bucket-section.tsx +94 -0
  5. package/src/board/dataSource.ts +44 -0
  6. package/src/board/facts.ts +183 -0
  7. package/src/board/index.ts +12 -0
  8. package/src/board/pull-request-board.tsx +239 -0
  9. package/src/board/pull-request-row.tsx +181 -0
  10. package/src/components/branch-diff/diff-themes.ts +6 -0
  11. package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
  12. package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
  13. package/src/components/branch-diff/diff-worker.ts +7 -0
  14. package/src/components/branch-diff/use-collapsed-files.ts +49 -0
  15. package/src/components/branch-diff/use-diff-settings.tsx +97 -0
  16. package/src/components/branch-diff/whitespace-button.tsx +32 -0
  17. package/src/components/copyable-code-block.tsx +101 -0
  18. package/src/components/label-pill.tsx +84 -0
  19. package/src/components/link-in-row.ts +6 -0
  20. package/src/components/markdown-suggestion.tsx +60 -0
  21. package/src/components/markdown-text.tsx +1067 -0
  22. package/src/components/pull-request-actions-merge.tsx +341 -0
  23. package/src/components/pull-request-actions-review.tsx +218 -0
  24. package/src/components/pull-request-checks.tsx +86 -0
  25. package/src/components/pull-request-commit-files.ts +143 -0
  26. package/src/components/pull-request-commits.tsx +221 -0
  27. package/src/components/pull-request-conversation-card.tsx +220 -0
  28. package/src/components/pull-request-conversation-edit.tsx +147 -0
  29. package/src/components/pull-request-conversation-event.tsx +267 -0
  30. package/src/components/pull-request-conversation-person.tsx +28 -0
  31. package/src/components/pull-request-conversation-thread.tsx +158 -0
  32. package/src/components/pull-request-diff-annotations.tsx +443 -0
  33. package/src/components/pull-request-diff-collapse.ts +43 -0
  34. package/src/components/pull-request-diff-data.ts +174 -0
  35. package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
  36. package/src/components/pull-request-diff-impacts.ts +45 -0
  37. package/src/components/pull-request-diff-jump.ts +87 -0
  38. package/src/components/pull-request-diff-lines.ts +103 -0
  39. package/src/components/pull-request-diff-overview.tsx +416 -0
  40. package/src/components/pull-request-diff-view.tsx +438 -0
  41. package/src/components/pull-request-diff-viewer.tsx +1374 -0
  42. package/src/components/pull-request-files.tsx +533 -0
  43. package/src/components/pull-request-markdown-input.tsx +91 -0
  44. package/src/components/pull-request-panel.tsx +1270 -0
  45. package/src/components/pull-request-part-notice.tsx +82 -0
  46. package/src/components/pull-request-reactions.tsx +178 -0
  47. package/src/components/pull-request-review.tsx +164 -0
  48. package/src/components/pull-request-revisions.tsx +273 -0
  49. package/src/components/pull-request-screen.tsx +299 -0
  50. package/src/components/pull-request-suggestion-actions.tsx +201 -0
  51. package/src/components/pull-request-timeline.tsx +270 -0
  52. package/src/components/repository-attachment-banner.tsx +42 -0
  53. package/src/components/scroll-parent.ts +50 -0
  54. package/src/css.d.ts +1 -0
  55. package/src/dataSource.ts +242 -0
  56. package/src/editor/alerts.ts +105 -0
  57. package/src/editor/code-editor.native.tsx +25 -0
  58. package/src/editor/code-editor.tsx +112 -0
  59. package/src/editor/codeEditorExtensions.ts +65 -0
  60. package/src/editor/codeHighlight.ts +71 -0
  61. package/src/editor/commands.ts +175 -0
  62. package/src/editor/decorations.ts +425 -0
  63. package/src/editor/editable-file.tsx +224 -0
  64. package/src/editor/emojiCompletion.ts +110 -0
  65. package/src/editor/extensions.ts +149 -0
  66. package/src/editor/frontmatter.ts +76 -0
  67. package/src/editor/inlineFormat.ts +114 -0
  68. package/src/editor/lineTypes.ts +55 -0
  69. package/src/editor/markdown-editor.css +511 -0
  70. package/src/editor/markdown-editor.native.tsx +29 -0
  71. package/src/editor/markdown-editor.tsx +295 -0
  72. package/src/editor/nodeClasses.ts +69 -0
  73. package/src/index.ts +75 -0
  74. package/src/markdown-github/flatten.ts +49 -0
  75. package/src/markdown-github/html-tokens.ts +93 -0
  76. package/src/markdown-github/pair-html.ts +354 -0
  77. package/src/markdown-github/safe-url.ts +15 -0
  78. package/src/markdown-github/table-columns.ts +23 -0
  79. package/src/markdown-github/toggle-task.ts +164 -0
  80. package/src/nativewind-env.d.ts +1 -0
  81. package/src/prStatus.ts +90 -0
  82. package/src/services/PullRequestDraftsProvider.tsx +179 -0
  83. package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
  84. package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
  85. package/src/services/emojify.ts +13 -0
  86. package/src/services/errorToMessage.ts +6 -0
  87. package/src/services/expiringDraftStore.ts +102 -0
  88. package/src/services/formatDataAge.ts +12 -0
  89. package/src/services/formatRelativeDate.ts +56 -0
  90. package/src/services/githubImages.tsx +33 -0
  91. package/src/services/isViewerAuthor.ts +7 -0
  92. package/src/slots.tsx +34 -0
@@ -0,0 +1,438 @@
1
+ import type {
2
+ DiffOmittedFile,
3
+ DiffStep,
4
+ DiffWhitespace,
5
+ PullRequestDiffResponse,
6
+ } from "@reviewflow/sdk";
7
+ import {
8
+ AccentScope,
9
+ Code,
10
+ IconButton,
11
+ IndeterminateCircularProgress,
12
+ IndeterminateLinearProgress,
13
+ InfoMessage,
14
+ Text,
15
+ View,
16
+ WarningMessage,
17
+ useCurrentBreakpointNameFiltered,
18
+ useCurrentMode,
19
+ } from "alouette";
20
+ import { ArrowsInLineVerticalRegularIcon } from "alouette-icons/phosphor-icons/ArrowsInLineVertical";
21
+ import { ArrowsOutLineVerticalRegularIcon } from "alouette-icons/phosphor-icons/ArrowsOutLineVertical";
22
+ import { ColumnsRegularIcon } from "alouette-icons/phosphor-icons/Columns";
23
+ import { RowsRegularIcon } from "alouette-icons/phosphor-icons/Rows";
24
+ import { TreeStructureRegularIcon } from "alouette-icons/phosphor-icons/TreeStructure";
25
+ import type { ComponentRef, ReactNode } from "react";
26
+ import {
27
+ Suspense,
28
+ lazy,
29
+ useDeferredValue,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ } from "react";
34
+ import type { PullRequestDiffRefusal } from "../dataSource.ts";
35
+ import { usePullRequestReviewDraft } from "../services/PullRequestReviewDraftProvider.tsx";
36
+ import { usePullRequestSlots } from "../slots.tsx";
37
+ import {
38
+ useDiffSettings,
39
+ useWhitespaceToggle,
40
+ } from "./branch-diff/use-diff-settings.tsx";
41
+ import { WhitespaceButton } from "./branch-diff/whitespace-button.tsx";
42
+ import { usePullRequestCollapse } from "./pull-request-diff-collapse.ts";
43
+ import type { PullRequestDiffTarget } from "./pull-request-diff-data.ts";
44
+ import {
45
+ useMissingCheckoutReason,
46
+ usePullRequestDiff,
47
+ usePullRequestImpacts,
48
+ usePullRequestReadingOrder,
49
+ } from "./pull-request-diff-data.ts";
50
+ import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
51
+ import {
52
+ DiffFileElementsContext,
53
+ createDiffFileElements,
54
+ } from "./pull-request-diff-jump.ts";
55
+ import type { Composer } from "./pull-request-diff-lines.ts";
56
+ import type { OverviewReadingOrderStatus } from "./pull-request-diff-overview.tsx";
57
+ import { PullRequestDiffOverview } from "./pull-request-diff-overview.tsx";
58
+ import type {
59
+ DiffStyle,
60
+ PullRequestDiffReview,
61
+ } from "./pull-request-diff-viewer.tsx";
62
+ import { cssVariableOf, scrollToTopOf } from "./scroll-parent.ts";
63
+
64
+ const PullRequestDiffViewer = lazy(
65
+ () => import("./pull-request-diff-viewer.tsx"),
66
+ );
67
+
68
+ const omissionLabel = (reason: DiffOmittedFile["reason"]): string => {
69
+ if (reason === "binary") return "binary";
70
+ if (reason === "tooLarge") return "too large";
71
+ if (reason === "whitespace") return "only whitespace changed";
72
+ return "over the diff budget";
73
+ };
74
+
75
+ function OmittedFiles({ omitted }: { omitted: DiffOmittedFile[] }): ReactNode {
76
+ if (omitted.length === 0) return null;
77
+ return (
78
+ <View className="gap-xxs">
79
+ <Text className="text-xs font-body-bold text-muted">
80
+ {`Left out (${String(omitted.length)})`}
81
+ </Text>
82
+ {omitted.map(({ path, reason }) => (
83
+ <View key={path} className="flex-row flex-wrap items-center gap-xs">
84
+ <Code className="text-xs">{path}</Code>
85
+ <Text className="text-xs text-muted">{omissionLabel(reason)}</Text>
86
+ </View>
87
+ ))}
88
+ </View>
89
+ );
90
+ }
91
+
92
+ interface DiffSourceProps {
93
+ diff: PullRequestDiffResponse;
94
+ asked: DiffWhitespace;
95
+ }
96
+
97
+ /**
98
+ * Said only when git could have answered and did not, or when GitHub could not
99
+ * hide whitespace as asked: a silent fallback hides a broken fetch, and a
100
+ * reindented file read as hidden would be read as rewritten.
101
+ */
102
+ function DiffSource({ diff, asked }: DiffSourceProps): ReactNode {
103
+ const fallback =
104
+ diff.source === "git" || diff.fallbackReason === null
105
+ ? null
106
+ : `Read from GitHub: local git could not (${diff.fallbackReason}).`;
107
+ const whitespace =
108
+ asked === "hide" && diff.whitespace === "show"
109
+ ? "Whitespace changes are shown: GitHub's API cannot hide them."
110
+ : null;
111
+ if (fallback === null && whitespace === null) return null;
112
+ return (
113
+ <InfoMessage>
114
+ {[fallback, whitespace].filter((line) => line !== null).join(" ")}
115
+ </InfoMessage>
116
+ );
117
+ }
118
+
119
+ const stepLabels: Record<DiffStep, string> = {
120
+ fetching: "fetching commits…",
121
+ diffing: "computing diff…",
122
+ };
123
+
124
+ /** What a read in progress is waiting on, when the source says: a large fetch takes tens of seconds. */
125
+ function DiffLoading({ step }: { step?: DiffStep | null }): ReactNode {
126
+ return (
127
+ <View className="items-center gap-xs py-xl">
128
+ <IndeterminateCircularProgress loading />
129
+ {step === null || step === undefined ? null : (
130
+ <Text className="text-sm text-muted">{stepLabels[step]}</Text>
131
+ )}
132
+ </View>
133
+ );
134
+ }
135
+
136
+ /**
137
+ * The source reads a diff from a local checkout only, and has none for this
138
+ * repository: says which file to edit and the line to add, rather than
139
+ * failing without a reason.
140
+ */
141
+ export function DiffRefused({
142
+ refused,
143
+ action,
144
+ }: {
145
+ refused: PullRequestDiffRefusal;
146
+ /** overrides the `renderRepositoryAction` slot: a button to where the checkout is configured */
147
+ action?: ReactNode;
148
+ }): ReactNode {
149
+ const { reason, ownerRepo, configPath, detail } = refused;
150
+ const { renderRepositoryAction } = usePullRequestSlots();
151
+ const shownAction = action ?? renderRepositoryAction?.(ownerRepo);
152
+ return (
153
+ <WarningMessage>
154
+ <View className="gap-xs">
155
+ <Text className="font-body-bold text-sm">
156
+ {reason === "notConfigured"
157
+ ? `${ownerRepo} has no local checkout configured, so its diff is not read.`
158
+ : `The configured checkout of ${ownerRepo} cannot be used.`}
159
+ </Text>
160
+ <Text className="text-sm">{detail}</Text>
161
+ <Text className="text-sm">
162
+ {"Add the line below to "}
163
+ <Code className="text-sm">{configPath}</Code>, under "repos":
164
+ </Text>
165
+ <Code className="text-sm">{`"${ownerRepo}": "/path/to/checkout"`}</Code>
166
+ {shownAction === null || shownAction === undefined ? null : (
167
+ <View className="flex-row">{shownAction}</View>
168
+ )}
169
+ </View>
170
+ </WarningMessage>
171
+ );
172
+ }
173
+
174
+ interface PullRequestDiffViewProps {
175
+ range: PullRequestDiffTarget;
176
+ /** left of the toolbar: a scope switch, a counter */
177
+ toolbarStart?: ReactNode;
178
+ /** last in the toolbar: the action the diff is read for */
179
+ toolbarEnd?: ReactNode;
180
+ /** absent: read-only, no threads, no commenting */
181
+ review?: PullRequestDiffReview;
182
+ }
183
+
184
+ /**
185
+ * One range of the pull request's diff with the toolbar every diff here has:
186
+ * split/unified, whitespace shown or hidden, open and close every file. Local
187
+ * git when the repository is here, GitHub's API otherwise
188
+ * (`PullRequestDiffResponse`).
189
+ *
190
+ * The toolbar sticks to the top of the scroll container, so what it carries
191
+ * stays in reach down a long diff; the file headers, sticky too, are pinned
192
+ * under it at its measured height rather than behind it. The loading bar is in
193
+ * it, laid over its bottom edge so its height never moves the headers.
194
+ *
195
+ * The viewer renders a deferred diff and is memoized: a toolbar press commits
196
+ * at once, and rendering every file of the next patch is interruptible work
197
+ * that happens after it, rather than a freeze before the press shows.
198
+ *
199
+ * Hiding whitespace is refused while a comment is being written: its line may
200
+ * leave the patch, and the comment with it.
201
+ */
202
+ export function PullRequestDiffView({
203
+ range,
204
+ toolbarStart,
205
+ toolbarEnd,
206
+ review,
207
+ }: PullRequestDiffViewProps): ReactNode {
208
+ const [diffStyle, setDiffStyle] = useState<DiffStyle>("split");
209
+ const [toolbarHeight, setToolbarHeight] = useState(0);
210
+ const fileElements = useMemo(createDiffFileElements, []);
211
+ const highlightProbe = useRef<ComponentRef<typeof View>>(null);
212
+ // a JS breakpoint rather than `xl:` classes: the sticky offset is the measured toolbar's
213
+ const aside = useCurrentBreakpointNameFiltered(["base", "wide"]) === "wide";
214
+ const whitespace = useWhitespaceToggle();
215
+ const { diff, error, loading, step, refused } = usePullRequestDiff(
216
+ range,
217
+ whitespace.whitespace,
218
+ );
219
+ const shownDiff = useDeferredValue(diff);
220
+ const collapse = usePullRequestCollapse();
221
+ const draft = usePullRequestReviewDraft();
222
+ // a commit's composer; the pull request's lives with the draft, which pins the range
223
+ const [commitComposer, setCommitComposer] = useState<{
224
+ commit: string;
225
+ composer: Composer;
226
+ } | null>(null);
227
+ const shownCommit = review?.commitOid ?? review?.commitFiles?.oid ?? null;
228
+ // kept per commit: stepping to the next commit does not carry it onto its lines
229
+ const shownCommitComposer =
230
+ commitComposer?.commit === shownCommit ? commitComposer.composer : null;
231
+ const onCommitComposerChange = useMemo(
232
+ () =>
233
+ (composer: Composer | null): void => {
234
+ setCommitComposer(
235
+ composer === null || shownCommit === null
236
+ ? null
237
+ : { commit: shownCommit, composer },
238
+ );
239
+ },
240
+ [shownCommit],
241
+ );
242
+ const composing =
243
+ review !== undefined &&
244
+ (shownCommitComposer !== null ||
245
+ (review.commitOid === undefined && draft.composer !== null));
246
+ const mode = useCurrentMode();
247
+ const { wordWrap, theme, fileOrder, setFileOrder } = useDiffSettings();
248
+ const missingCheckout = useMissingCheckoutReason(range);
249
+ const readingOrder = usePullRequestReadingOrder(
250
+ range,
251
+ missingCheckout === null,
252
+ );
253
+ const ordered =
254
+ readingOrder.available &&
255
+ missingCheckout === null &&
256
+ fileOrder === "reading";
257
+ const impacts = usePullRequestImpacts(range, missingCheckout === null);
258
+ const impactsStatus = ((): ImpactsStatus => {
259
+ if (impacts.value !== null) return "ready";
260
+ if (impacts.reading) return "reading";
261
+ if (impacts.error !== null) return "failed";
262
+ return "none";
263
+ })();
264
+ const overviewStatus = ((): OverviewReadingOrderStatus => {
265
+ if (readingOrder.value !== null) return "ready";
266
+ if (readingOrder.reading) return "reading";
267
+ if (readingOrder.error !== null) return "failed";
268
+ return "none";
269
+ })();
270
+
271
+ return (
272
+ <View
273
+ className={
274
+ aside && diffStyle === "split"
275
+ ? "gap-m web:relative web:left-1/2 web:w-[min(100vw-3rem,1800px)] web:-translate-x-1/2"
276
+ : "gap-m"
277
+ }
278
+ >
279
+ <View
280
+ className="-mx-l flex-row flex-wrap items-center justify-between gap-xs bg-screen px-l py-sm web:sticky web:top-0 web:z-10"
281
+ onLayout={(event) => {
282
+ setToolbarHeight(event.nativeEvent.layout.height);
283
+ }}
284
+ >
285
+ {/* sized to the bar: a zero-height box under the sticky toolbar left
286
+ Chromium painting the faded bar on, at its last width */}
287
+ <View className="absolute inset-x-l bottom-0 h-0.5">
288
+ <IndeterminateLinearProgress
289
+ loading={
290
+ loading || (ordered && readingOrder.reading) || shownDiff !== diff
291
+ }
292
+ size="xs"
293
+ />
294
+ </View>
295
+ <View className="flex-row flex-wrap items-center gap-sm">
296
+ {toolbarStart}
297
+ </View>
298
+ <View className="flex-row items-center gap-xs">
299
+ <IconButton
300
+ icon={
301
+ diffStyle === "split" ? (
302
+ <RowsRegularIcon />
303
+ ) : (
304
+ <ColumnsRegularIcon />
305
+ )
306
+ }
307
+ size="sm"
308
+ aria-label={
309
+ diffStyle === "split" ? "Show unified diff" : "Show split diff"
310
+ }
311
+ onPress={() => {
312
+ setDiffStyle(diffStyle === "split" ? "unified" : "split");
313
+ }}
314
+ />
315
+ {readingOrder.available ? (
316
+ <IconButton
317
+ icon={<TreeStructureRegularIcon />}
318
+ size="sm"
319
+ accent={ordered ? "brand" : "neutral"}
320
+ disabledReason={missingCheckout}
321
+ aria-label={
322
+ ordered
323
+ ? "Order files by package"
324
+ : "Order files for reading (experimental)"
325
+ }
326
+ onPress={() => {
327
+ setFileOrder(ordered ? "packages" : "reading");
328
+ }}
329
+ />
330
+ ) : null}
331
+ <WhitespaceButton
332
+ whitespace={whitespace.whitespace}
333
+ disabledReason={
334
+ composing ? "Post or close the comment being written first" : null
335
+ }
336
+ onToggle={whitespace.toggle}
337
+ />
338
+ <IconButton
339
+ icon={<ArrowsOutLineVerticalRegularIcon />}
340
+ size="sm"
341
+ aria-label="Open every file"
342
+ onPress={collapse.expandAll}
343
+ />
344
+ <IconButton
345
+ icon={<ArrowsInLineVerticalRegularIcon />}
346
+ size="sm"
347
+ aria-label="Close every file"
348
+ onPress={collapse.collapseAll}
349
+ />
350
+ {toolbarEnd}
351
+ </View>
352
+ </View>
353
+ {error === null ? null : <WarningMessage>{error}</WarningMessage>}
354
+ {ordered && readingOrder.error !== null ? (
355
+ <WarningMessage>
356
+ {`Files are in package order: the reading order failed (${readingOrder.error})`}
357
+ </WarningMessage>
358
+ ) : null}
359
+ {refused === null ? null : <DiffRefused refused={refused} />}
360
+ {(() => {
361
+ if (shownDiff === null) {
362
+ if (refused !== null || error !== null) return null;
363
+ return <DiffLoading step={step} />;
364
+ }
365
+ return (
366
+ <View className="gap-m">
367
+ <DiffSource diff={shownDiff} asked={whitespace.whitespace} />
368
+ <View className={aside ? "flex-row items-start gap-xl" : "gap-l"}>
369
+ <View
370
+ className={
371
+ aside
372
+ ? "w-[280px] shrink-0 web:sticky web:max-h-[calc(100vh-8rem)] web:overflow-y-auto"
373
+ : ""
374
+ }
375
+ style={aside ? { top: toolbarHeight } : undefined}
376
+ >
377
+ <PullRequestDiffOverview
378
+ groups={shownDiff.groups}
379
+ omitted={shownDiff.omitted}
380
+ readingOrder={readingOrder.value}
381
+ readingOrderStatus={overviewStatus}
382
+ impacts={impacts.value}
383
+ impactsStatus={impactsStatus}
384
+ displayOrder={ordered ? readingOrder.value : null}
385
+ aside={aside}
386
+ onFilePress={(path) => {
387
+ const element = fileElements.elementOf(path);
388
+ if (element === undefined) return;
389
+ scrollToTopOf(element, toolbarHeight);
390
+ const color = cssVariableOf(
391
+ highlightProbe.current,
392
+ "--color-enabled",
393
+ );
394
+ if (color !== "") fileElements.highlight(path, color);
395
+ }}
396
+ />
397
+ {/* the file header is pierre's shadow DOM, which no accent
398
+ class reaches: the color is read here and handed to it */}
399
+ <AccentScope accent="info">
400
+ <View ref={highlightProbe} className="hidden" />
401
+ </AccentScope>
402
+ </View>
403
+ <View className="min-w-0 flex-1 gap-m">
404
+ <OmittedFiles omitted={shownDiff.omitted} />
405
+ {shownDiff.patch === "" ? (
406
+ <Text className="text-muted">no changes</Text>
407
+ ) : (
408
+ <Suspense fallback={<DiffLoading />}>
409
+ <DiffFileElementsContext value={fileElements}>
410
+ <PullRequestDiffViewer
411
+ pullRequest={range}
412
+ patch={shownDiff.patch}
413
+ groups={shownDiff.groups}
414
+ readingOrder={ordered ? readingOrder.value : null}
415
+ impacts={impacts.value}
416
+ sides={shownDiff.sides}
417
+ whitespace={shownDiff.whitespace}
418
+ diffStyle={diffStyle}
419
+ theme={theme}
420
+ themeType={mode}
421
+ wordWrap={wordWrap}
422
+ stickyTop={toolbarHeight}
423
+ collapse={collapse}
424
+ review={review}
425
+ commitComposer={shownCommitComposer}
426
+ onCommitComposerChange={onCommitComposerChange}
427
+ />
428
+ </DiffFileElementsContext>
429
+ </Suspense>
430
+ )}
431
+ </View>
432
+ </View>
433
+ </View>
434
+ );
435
+ })()}
436
+ </View>
437
+ );
438
+ }