@reviewflow/ui 0.0.0-stage → 6.22.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 +101 -3
  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,97 @@
1
+ import type { DiffImpactsResponse } from "@reviewflow/sdk";
2
+ import { AccentScope, Icon, Text, View } from "alouette";
3
+ import { InfoRegularIcon } from "alouette-icons/phosphor-icons/Info";
4
+ import { ShareNetworkDuotoneIcon } from "alouette-icons/phosphor-icons/ShareNetwork";
5
+ import type { ReactNode } from "react";
6
+ import type { ImpactsStatus } from "./pull-request-diff-impacts.ts";
7
+ import { countImpacts, pluralOf } from "./pull-request-diff-impacts.ts";
8
+
9
+ interface PullRequestDiffImpactsSummaryProps {
10
+ /** null until read, or when it could not be */
11
+ impacts: DiffImpactsResponse | null;
12
+ status: ImpactsStatus;
13
+ }
14
+
15
+ /** Past this many, the files not analyzed are counted rather than named. */
16
+ const maxNotAnalyzedNames = 5;
17
+
18
+ const notAnalyzedFilesLine = (paths: readonly string[]): string =>
19
+ paths.length > maxNotAnalyzedNames
20
+ ? `${pluralOf(paths.length, "file")} not analyzed (not TypeScript, or deleted).`
21
+ : `Not analyzed (not TypeScript, or deleted): ${paths.join(", ")}.`;
22
+
23
+ const notAnalyzedLines = (impacts: DiffImpactsResponse): string[] => {
24
+ if (impacts.tooLarge) {
25
+ return [
26
+ impacts.reason === "files"
27
+ ? "Impacts not analyzed: the repository is too large."
28
+ : "Impacts not analyzed yet: reading the repository is taking long, trying again…",
29
+ ];
30
+ }
31
+ return [
32
+ ...(impacts.notAnalyzed.length === 0
33
+ ? []
34
+ : [notAnalyzedFilesLine(impacts.notAnalyzed)]),
35
+ ...(impacts.unresolvedIn.length === 0
36
+ ? []
37
+ : [
38
+ `Importers in ${impacts.unresolvedIn.join(", ")} not analyzed: some imports resolve to no file.`,
39
+ ]),
40
+ ];
41
+ };
42
+
43
+ /**
44
+ * What the change's touched exports are used by outside the diff, and what
45
+ * could not be analyzed. While reading or after a failure it says so: never a
46
+ * count it does not know.
47
+ */
48
+ export function PullRequestDiffImpactsSummary({
49
+ impacts,
50
+ status,
51
+ }: PullRequestDiffImpactsSummaryProps): ReactNode {
52
+ if (impacts === null) {
53
+ if (status === "reading") {
54
+ return <Text className="text-xs text-muted">Reading the impacts…</Text>;
55
+ }
56
+ if (status === "failed") {
57
+ return (
58
+ <Text className="text-xs text-muted">
59
+ The impacts could not be read.
60
+ </Text>
61
+ );
62
+ }
63
+ return null;
64
+ }
65
+ const lines = notAnalyzedLines(impacts);
66
+ const counts = impacts.tooLarge ? null : countImpacts(impacts.files);
67
+ return (
68
+ <View className="gap-xs">
69
+ {counts === null ? null : (
70
+ <View className="flex-row items-start gap-xs">
71
+ <AccentScope accent="info">
72
+ <View className="pt-[2px]">
73
+ <Icon
74
+ icon={<ShareNetworkDuotoneIcon />}
75
+ size={18}
76
+ className="text-accent"
77
+ />
78
+ </View>
79
+ </AccentScope>
80
+ <Text className="shrink text-sm text-sharp">
81
+ {counts.exports === 0
82
+ ? "No touched export is used outside the diff."
83
+ : `${pluralOf(counts.exports, "touched export")} used in ${pluralOf(counts.files, "file")}${counts.tests === 0 ? "" : `, ${pluralOf(counts.tests, "test")}`} outside the diff.`}
84
+ </Text>
85
+ </View>
86
+ )}
87
+ {lines.map((line) => (
88
+ <View key={line} className="flex-row items-start gap-xs">
89
+ <View className="pt-[1px]">
90
+ <Icon icon={<InfoRegularIcon />} size={16} className="text-muted" />
91
+ </View>
92
+ <Text className="shrink text-xs text-muted">{line}</Text>
93
+ </View>
94
+ ))}
95
+ </View>
96
+ );
97
+ }
@@ -0,0 +1,45 @@
1
+ import type { DiffFileImpacts } from "@reviewflow/sdk";
2
+
3
+ /** `none`: no checkout, or a source that cannot read impacts. */
4
+ export type ImpactsStatus = "failed" | "none" | "reading" | "ready";
5
+
6
+ export interface ImpactCounts {
7
+ /** touched exports used outside the diff */
8
+ exports: number;
9
+ /** distinct importing files, tests apart */
10
+ files: number;
11
+ tests: number;
12
+ removed: boolean;
13
+ }
14
+
15
+ export const countImpacts = (
16
+ impacts: readonly DiffFileImpacts[],
17
+ ): ImpactCounts => {
18
+ const files = new Set<string>();
19
+ const tests = new Set<string>();
20
+ let exports = 0;
21
+ let removed = false;
22
+ for (const file of impacts) {
23
+ for (const impact of file.exports) {
24
+ exports += 1;
25
+ removed ||= impact.removed;
26
+ for (const importer of impact.importers) {
27
+ (importer.test ? tests : files).add(importer.path);
28
+ }
29
+ }
30
+ }
31
+ return { exports, files: files.size, tests: tests.size, removed };
32
+ };
33
+
34
+ /** Past this many files using a changed file's exports, each export carries its counts only. */
35
+ export const maxImporterNames = 50;
36
+
37
+ export const pluralOf = (count: number, word: string): string =>
38
+ `${String(count)} ${word}${count === 1 ? "" : "s"}`;
39
+
40
+ /** "4 files · 2 tests", either part left out when it is 0. */
41
+ export const usageLabel = ({ files, tests }: ImpactCounts): string =>
42
+ [
43
+ ...(files > 0 || tests === 0 ? [pluralOf(files, "file")] : []),
44
+ ...(tests > 0 ? [pluralOf(tests, "test")] : []),
45
+ ].join(" · ");
@@ -0,0 +1,87 @@
1
+ import { animationDurationsMs } from "alouette";
2
+ import { createContext, use, useSyncExternalStore } from "react";
3
+
4
+ /** How long a file gone to stays outlined before the outline fades. */
5
+ const highlightHoldMs = 1500;
6
+
7
+ export interface DiffFileHighlight {
8
+ /** `shown` then `fading`, then no highlight at all */
9
+ phase: "fading" | "shown";
10
+ /** a CSS color, read from the theme when the file was gone to */
11
+ color: string;
12
+ }
13
+
14
+ /**
15
+ * The rendered files of a diff by path, so a link elsewhere on the screen (the
16
+ * overview) can bring one into view and outline it a moment, to show which
17
+ * one it is. The viewer registers its files; the diff view owns the registry.
18
+ */
19
+ export interface DiffFileElements {
20
+ /** returns the unregistration */
21
+ register: (path: string, element: HTMLElement) => () => void;
22
+ elementOf: (path: string) => HTMLElement | undefined;
23
+ /** outlines the file's header in `color`, replacing any outline still showing */
24
+ highlight: (path: string, color: string) => void;
25
+ subscribe: (listener: () => void) => () => void;
26
+ highlightOf: (path: string) => DiffFileHighlight | null;
27
+ }
28
+
29
+ export const createDiffFileElements = (): DiffFileElements => {
30
+ const elements = new Map<string, HTMLElement>();
31
+ const listeners = new Set<() => void>();
32
+ let highlighted: (DiffFileHighlight & { path: string }) | null = null;
33
+ let timers: ReturnType<typeof setTimeout>[] = [];
34
+ const setHighlighted = (next: typeof highlighted): void => {
35
+ highlighted = next;
36
+ for (const listener of listeners) listener();
37
+ };
38
+ return {
39
+ register: (path, element) => {
40
+ elements.set(path, element);
41
+ return () => {
42
+ if (elements.get(path) === element) elements.delete(path);
43
+ };
44
+ },
45
+ elementOf: (path) => elements.get(path),
46
+ highlight: (path, color) => {
47
+ for (const timer of timers) clearTimeout(timer);
48
+ setHighlighted({ path, phase: "shown", color });
49
+ timers = [
50
+ setTimeout(() => {
51
+ setHighlighted({ path, phase: "fading", color });
52
+ }, highlightHoldMs),
53
+ setTimeout(() => {
54
+ setHighlighted(null);
55
+ }, highlightHoldMs + animationDurationsMs.collapse),
56
+ ];
57
+ },
58
+ subscribe: (listener) => {
59
+ listeners.add(listener);
60
+ return () => {
61
+ listeners.delete(listener);
62
+ };
63
+ },
64
+ // the same object until it changes, as useSyncExternalStore needs
65
+ highlightOf: (path) => (highlighted?.path === path ? highlighted : null),
66
+ };
67
+ };
68
+
69
+ export const DiffFileElementsContext = createContext<DiffFileElements | null>(
70
+ null,
71
+ );
72
+
73
+ export const useDiffFileElements = (): DiffFileElements | null =>
74
+ use(DiffFileElementsContext);
75
+
76
+ const noSubscription = (): (() => void) => () => {};
77
+
78
+ /** Re-renders only the file whose highlight changes, not every file of the diff. */
79
+ export const useDiffFileHighlight = (
80
+ path: string,
81
+ ): DiffFileHighlight | null => {
82
+ const fileElements = useDiffFileElements();
83
+ return useSyncExternalStore(
84
+ fileElements?.subscribe ?? noSubscription,
85
+ () => fileElements?.highlightOf(path) ?? null,
86
+ );
87
+ };
@@ -0,0 +1,103 @@
1
+ import type { FileDiffMetadata, SelectedLineRange } from "@pierre/diffs";
2
+ import type { PullRequestDiffSide } from "@reviewflow/sdk";
3
+
4
+ export interface LineAnchor {
5
+ path: string;
6
+ side: PullRequestDiffSide;
7
+ line: number;
8
+ /** the first line of a range; null for one line */
9
+ startLine: number | null;
10
+ startSide: PullRequestDiffSide | null;
11
+ }
12
+
13
+ /** The comment being written on a diff, at most one at a time. */
14
+ export interface Composer {
15
+ fileKey: string;
16
+ anchor: LineAnchor;
17
+ range: SelectedLineRange;
18
+ }
19
+
20
+ export const annotationSideOf = (
21
+ side: PullRequestDiffSide,
22
+ ): "additions" | "deletions" => (side === "RIGHT" ? "additions" : "deletions");
23
+
24
+ const sideOf = (side: "additions" | "deletions"): PullRequestDiffSide =>
25
+ side === "additions" ? "RIGHT" : "LEFT";
26
+
27
+ /**
28
+ * A drag can end above where it started. Within one side that is a swap; across
29
+ * the two sides of a unified diff the order is the reader's and is kept.
30
+ */
31
+ export const anchorOfRange = (
32
+ path: string,
33
+ { start, end, side = "additions", endSide }: SelectedLineRange,
34
+ ): LineAnchor => {
35
+ const last = endSide ?? side;
36
+ const swapped = side === last && start > end;
37
+ const from = swapped ? end : start;
38
+ const to = swapped ? start : end;
39
+ const single = from === to && side === last;
40
+ return {
41
+ path,
42
+ side: sideOf(last),
43
+ line: to,
44
+ startLine: single ? null : from,
45
+ startSide: single ? null : sideOf(side),
46
+ };
47
+ };
48
+
49
+ const stripEol = (line: string): string => line.replace(/\r?\n$/, "");
50
+
51
+ const rightLine = (
52
+ file: FileDiffMetadata,
53
+ lineNumber: number,
54
+ ): string | null => {
55
+ const hunk = file.hunks.find(
56
+ ({ additionStart, additionCount }) =>
57
+ lineNumber >= additionStart && lineNumber < additionStart + additionCount,
58
+ );
59
+ if (hunk === undefined) return null;
60
+ const line =
61
+ file.additionLines[
62
+ hunk.additionLineIndex + lineNumber - hunk.additionStart
63
+ ];
64
+ return line === undefined ? null : stripEol(line);
65
+ };
66
+
67
+ /**
68
+ * The new-file lines an anchor covers, for a suggestion block. Null when the
69
+ * range touches the old side or a line the patch does not carry (a gap
70
+ * between hunks), where a suggestion would replace text it never showed.
71
+ */
72
+ export const rightSideLines = (
73
+ file: FileDiffMetadata,
74
+ anchor: LineAnchor,
75
+ ): string[] | null => {
76
+ if (anchor.side !== "RIGHT") return null;
77
+ if (anchor.startSide !== null && anchor.startSide !== "RIGHT") return null;
78
+ const first = anchor.startLine ?? anchor.line;
79
+ const lines: string[] = [];
80
+ for (let number = first; number <= anchor.line; number += 1) {
81
+ const line = rightLine(file, number);
82
+ if (line === null) return null;
83
+ lines.push(line);
84
+ }
85
+ return lines;
86
+ };
87
+
88
+ /** Whether a hunk of the patch carries the line on that side, context included. */
89
+ export const isLineInPatch = (
90
+ file: FileDiffMetadata,
91
+ side: PullRequestDiffSide,
92
+ line: number,
93
+ ): boolean =>
94
+ file.hunks.some((hunk) =>
95
+ side === "RIGHT"
96
+ ? line >= hunk.additionStart &&
97
+ line < hunk.additionStart + hunk.additionCount
98
+ : line >= hunk.deletionStart &&
99
+ line < hunk.deletionStart + hunk.deletionCount,
100
+ );
101
+
102
+ export const suggestionBlock = (lines: string[]): string =>
103
+ `\`\`\`suggestion\n${lines.join("\n")}\n\`\`\``;