@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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { DiffWhitespace } from "@reviewflow/sdk";
|
|
1
|
+
import type { DiffFont, DiffWhitespace } from "@reviewflow/sdk";
|
|
2
|
+
import { defaultDiffFont, defaultDiffTheme } from "@reviewflow/sdk";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import { createContext, use, useMemo, useState } from "react";
|
|
5
|
+
import { diffFontFamily } from "./diff-font.ts";
|
|
4
6
|
import type { DiffTheme } from "./diff-themes.ts";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -16,6 +18,8 @@ export const isDiffFileOrder = (value: string): value is DiffFileOrder =>
|
|
|
16
18
|
export interface DiffSettings {
|
|
17
19
|
wordWrap: boolean;
|
|
18
20
|
theme: DiffTheme;
|
|
21
|
+
/** the code's CSS `font-family`; null keeps pierre's own */
|
|
22
|
+
fontFamily: string | null;
|
|
19
23
|
whitespace: DiffWhitespace;
|
|
20
24
|
setWhitespace: (whitespace: DiffWhitespace) => void;
|
|
21
25
|
fileOrder: DiffFileOrder;
|
|
@@ -24,7 +28,8 @@ export interface DiffSettings {
|
|
|
24
28
|
|
|
25
29
|
const defaults: DiffSettings = {
|
|
26
30
|
wordWrap: true,
|
|
27
|
-
theme:
|
|
31
|
+
theme: defaultDiffTheme,
|
|
32
|
+
fontFamily: diffFontFamily(defaultDiffFont, ""),
|
|
28
33
|
whitespace: "show",
|
|
29
34
|
setWhitespace: () => undefined,
|
|
30
35
|
fileOrder: "packages",
|
|
@@ -35,6 +40,9 @@ const DiffSettingsContext = createContext<DiffSettings>(defaults);
|
|
|
35
40
|
|
|
36
41
|
interface DiffSettingsProviderProps {
|
|
37
42
|
theme?: DiffTheme;
|
|
43
|
+
font?: DiffFont;
|
|
44
|
+
/** the CSS `font-family` of the `custom` font */
|
|
45
|
+
customFont?: string;
|
|
38
46
|
wordWrap?: boolean;
|
|
39
47
|
/** the whitespace the diff starts with; the toolbar toggle then owns it */
|
|
40
48
|
initialWhitespace?: DiffWhitespace;
|
|
@@ -52,6 +60,8 @@ interface DiffSettingsProviderProps {
|
|
|
52
60
|
*/
|
|
53
61
|
export function DiffSettingsProvider({
|
|
54
62
|
theme = defaults.theme,
|
|
63
|
+
font = defaultDiffFont,
|
|
64
|
+
customFont = "",
|
|
55
65
|
wordWrap = defaults.wordWrap,
|
|
56
66
|
initialWhitespace = defaults.whitespace,
|
|
57
67
|
fileOrder: storedFileOrder,
|
|
@@ -61,9 +71,11 @@ export function DiffSettingsProvider({
|
|
|
61
71
|
const [whitespace, setWhitespace] = useState(initialWhitespace);
|
|
62
72
|
const [ownFileOrder, setOwnFileOrder] = useState(defaults.fileOrder);
|
|
63
73
|
const fileOrder = storedFileOrder ?? ownFileOrder;
|
|
74
|
+
const fontFamily = diffFontFamily(font, customFont);
|
|
64
75
|
const value = useMemo(
|
|
65
76
|
() => ({
|
|
66
77
|
theme,
|
|
78
|
+
fontFamily,
|
|
67
79
|
wordWrap,
|
|
68
80
|
whitespace,
|
|
69
81
|
setWhitespace,
|
|
@@ -73,7 +85,7 @@ export function DiffSettingsProvider({
|
|
|
73
85
|
onFileOrderChange?.(next);
|
|
74
86
|
},
|
|
75
87
|
}),
|
|
76
|
-
[theme, wordWrap, whitespace, fileOrder, onFileOrderChange],
|
|
88
|
+
[theme, fontFamily, wordWrap, whitespace, fileOrder, onFileOrderChange],
|
|
77
89
|
);
|
|
78
90
|
return <DiffSettingsContext value={value}>{children}</DiffSettingsContext>;
|
|
79
91
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DiffWhitespace } from "@reviewflow/sdk";
|
|
2
|
-
import { IconButton } from "alouette";
|
|
3
2
|
import { ParagraphRegularIcon } from "alouette-icons/phosphor-icons/Paragraph";
|
|
4
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { TooltipIconButton } from "../tooltip-icon-button.tsx";
|
|
5
5
|
|
|
6
6
|
interface WhitespaceButtonProps {
|
|
7
7
|
whitespace: DiffWhitespace;
|
|
@@ -16,7 +16,7 @@ export function WhitespaceButton({
|
|
|
16
16
|
onToggle,
|
|
17
17
|
}: WhitespaceButtonProps): ReactNode {
|
|
18
18
|
return (
|
|
19
|
-
<
|
|
19
|
+
<TooltipIconButton
|
|
20
20
|
icon={<ParagraphRegularIcon />}
|
|
21
21
|
size="sm"
|
|
22
22
|
accent={whitespace === "hide" ? "brand" : "neutral"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { DiffFont, DiffTheme } from "@reviewflow/sdk";
|
|
2
|
+
import {
|
|
3
|
+
diffFontOptions,
|
|
4
|
+
diffThemeOptions,
|
|
5
|
+
isDiffFont,
|
|
6
|
+
isDiffTheme,
|
|
7
|
+
} from "@reviewflow/sdk";
|
|
8
|
+
import {
|
|
9
|
+
Box,
|
|
10
|
+
ErrorMessage,
|
|
11
|
+
FormItem,
|
|
12
|
+
IndeterminateCircularProgress,
|
|
13
|
+
InputText,
|
|
14
|
+
Select,
|
|
15
|
+
Switch,
|
|
16
|
+
Text,
|
|
17
|
+
View,
|
|
18
|
+
useCurrentMode,
|
|
19
|
+
} from "alouette";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { Suspense, lazy, useState } from "react";
|
|
22
|
+
import { errorToMessage } from "../services/errorToMessage.ts";
|
|
23
|
+
import { diffFontFamily } from "./branch-diff/diff-font.ts";
|
|
24
|
+
|
|
25
|
+
const DiffThemePreview = lazy(() => import("./diff-theme-preview.tsx"));
|
|
26
|
+
|
|
27
|
+
export interface DiffDisplaySettings {
|
|
28
|
+
wordWrap: boolean;
|
|
29
|
+
hideWhitespace: boolean;
|
|
30
|
+
theme: DiffTheme;
|
|
31
|
+
font: DiffFont;
|
|
32
|
+
/** the CSS `font-family` of the `custom` font */
|
|
33
|
+
customFont: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DiffSettingsSectionProps {
|
|
37
|
+
settings: DiffDisplaySettings;
|
|
38
|
+
/** saves one change, wherever the app keeps them; a rejection is shown in the section */
|
|
39
|
+
onChange: (change: Partial<DiffDisplaySettings>) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How the diffs render, for an app's settings screen. Saved on the press; the
|
|
44
|
+
* preview follows the picked theme at once rather than waiting for the save.
|
|
45
|
+
*/
|
|
46
|
+
export function DiffSettingsSection({
|
|
47
|
+
settings,
|
|
48
|
+
onChange,
|
|
49
|
+
}: DiffSettingsSectionProps): ReactNode {
|
|
50
|
+
const mode = useCurrentMode();
|
|
51
|
+
const [error, setError] = useState<string | null>(null);
|
|
52
|
+
const [pickedTheme, setPickedTheme] = useState<DiffTheme | null>(null);
|
|
53
|
+
const [pickedFont, setPickedFont] = useState<DiffFont | null>(null);
|
|
54
|
+
const [typedCustomFont, setTypedCustomFont] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
const save = (
|
|
57
|
+
change: Partial<DiffDisplaySettings>,
|
|
58
|
+
onSettled?: () => void,
|
|
59
|
+
): void => {
|
|
60
|
+
setError(null);
|
|
61
|
+
onChange(change)
|
|
62
|
+
.catch((changeError: unknown) => {
|
|
63
|
+
setError(errorToMessage(changeError));
|
|
64
|
+
})
|
|
65
|
+
.finally(onSettled);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const previewTheme = pickedTheme ?? settings.theme;
|
|
69
|
+
const previewFont = pickedFont ?? settings.font;
|
|
70
|
+
const previewCustomFont = typedCustomFont ?? settings.customFont;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Box className="surface gap-m p-m">
|
|
74
|
+
<Text className="font-heading-bold text-lg">Diff</Text>
|
|
75
|
+
|
|
76
|
+
<FormItem
|
|
77
|
+
label="Wrap long lines"
|
|
78
|
+
details="On, a line longer than its column wraps. Off, it stays on one line and the file scrolls sideways."
|
|
79
|
+
render={({ labelId, describedBy }) => (
|
|
80
|
+
<Switch
|
|
81
|
+
aria-labelledby={labelId}
|
|
82
|
+
aria-describedby={describedBy}
|
|
83
|
+
checked={settings.wordWrap}
|
|
84
|
+
onValueChange={(wordWrap) => {
|
|
85
|
+
save({ wordWrap });
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<FormItem
|
|
92
|
+
label="Hide whitespace changes"
|
|
93
|
+
details="On, a line whose only change is whitespace is not shown as changed, and a file with nothing else is listed as left out. The diff's toolbar switches it too. A pull request read from GitHub's API shows whitespace whatever this says."
|
|
94
|
+
render={({ labelId, describedBy }) => (
|
|
95
|
+
<Switch
|
|
96
|
+
aria-labelledby={labelId}
|
|
97
|
+
aria-describedby={describedBy}
|
|
98
|
+
checked={settings.hideWhitespace}
|
|
99
|
+
onValueChange={(hideWhitespace) => {
|
|
100
|
+
save({ hideWhitespace });
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<FormItem
|
|
107
|
+
label="Theme"
|
|
108
|
+
details="The syntax colours of the diffs. Each theme has a light and a dark variant, following the app's mode."
|
|
109
|
+
render={({ labelId, describedBy }) => (
|
|
110
|
+
<Select
|
|
111
|
+
aria-labelledby={labelId}
|
|
112
|
+
aria-describedby={describedBy}
|
|
113
|
+
options={diffThemeOptions}
|
|
114
|
+
value={previewTheme}
|
|
115
|
+
onValueChange={(theme) => {
|
|
116
|
+
if (!isDiffTheme(theme)) return;
|
|
117
|
+
setPickedTheme(theme);
|
|
118
|
+
save({ theme }, () => {
|
|
119
|
+
setPickedTheme((current) =>
|
|
120
|
+
current === theme ? null : current,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<FormItem
|
|
129
|
+
label="Font"
|
|
130
|
+
details="The font of the diffs' code: the app's own mono, the system mono font the diff library ships with, or one installed on this machine."
|
|
131
|
+
render={({ labelId, describedBy }) => (
|
|
132
|
+
<Select
|
|
133
|
+
aria-labelledby={labelId}
|
|
134
|
+
aria-describedby={describedBy}
|
|
135
|
+
options={diffFontOptions}
|
|
136
|
+
value={previewFont}
|
|
137
|
+
onValueChange={(font) => {
|
|
138
|
+
if (!isDiffFont(font)) return;
|
|
139
|
+
setPickedFont(font);
|
|
140
|
+
save({ font }, () => {
|
|
141
|
+
setPickedFont((current) => (current === font ? null : current));
|
|
142
|
+
});
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
{previewFont === "custom" ? (
|
|
149
|
+
<FormItem
|
|
150
|
+
label="Custom font"
|
|
151
|
+
details="A CSS font-family, as installed on this machine: JetBrains Mono, or a list such as “Fira Code”, Menlo. The app's mono stands in for a font this machine does not have."
|
|
152
|
+
render={({ labelId, describedBy }) => (
|
|
153
|
+
<InputText
|
|
154
|
+
value={previewCustomFont}
|
|
155
|
+
aria-labelledby={labelId}
|
|
156
|
+
aria-describedby={describedBy}
|
|
157
|
+
autoComplete="off"
|
|
158
|
+
placeholder="JetBrains Mono"
|
|
159
|
+
onChangeText={(customFont) => {
|
|
160
|
+
setTypedCustomFont(customFont);
|
|
161
|
+
save({ customFont }, () => {
|
|
162
|
+
setTypedCustomFont((current) =>
|
|
163
|
+
current === customFont ? null : current,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
) : null}
|
|
171
|
+
|
|
172
|
+
<Suspense
|
|
173
|
+
fallback={
|
|
174
|
+
<View className="items-center py-m">
|
|
175
|
+
<IndeterminateCircularProgress loading />
|
|
176
|
+
</View>
|
|
177
|
+
}
|
|
178
|
+
>
|
|
179
|
+
<DiffThemePreview
|
|
180
|
+
theme={previewTheme}
|
|
181
|
+
fontFamily={diffFontFamily(previewFont, previewCustomFont)}
|
|
182
|
+
themeType={mode}
|
|
183
|
+
/>
|
|
184
|
+
</Suspense>
|
|
185
|
+
|
|
186
|
+
{error === null ? null : <ErrorMessage>{error}</ErrorMessage>}
|
|
187
|
+
</Box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parseDiffFromFile } from "@pierre/diffs";
|
|
2
|
+
import { FileDiff } from "@pierre/diffs/react";
|
|
3
|
+
import type { DiffTheme } from "@reviewflow/sdk";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { diffFontCSS } from "./branch-diff/diff-font.ts";
|
|
6
|
+
import {
|
|
7
|
+
changeBackgroundCSS,
|
|
8
|
+
diffThemeNames,
|
|
9
|
+
} from "./branch-diff/diff-themes.ts";
|
|
10
|
+
|
|
11
|
+
const before = `import { readFile } from "node:fs/promises";
|
|
12
|
+
|
|
13
|
+
/** Reads the settings, or null when there are none. */
|
|
14
|
+
export async function loadSettings(path: string) {
|
|
15
|
+
const contents = await readFile(path, "utf8");
|
|
16
|
+
return JSON.parse(contents);
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const after = `import { readFile } from "node:fs/promises";
|
|
21
|
+
|
|
22
|
+
const defaultTheme = "github";
|
|
23
|
+
|
|
24
|
+
/** Reads the settings, or the defaults when there are none. */
|
|
25
|
+
export async function loadSettings(path: string): Promise<Settings> {
|
|
26
|
+
const contents = await readFile(path, "utf8").catch(() => "{}");
|
|
27
|
+
return { theme: defaultTheme, ...JSON.parse(contents) };
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const sample = parseDiffFromFile(
|
|
32
|
+
{ name: "settings.ts", contents: before },
|
|
33
|
+
{ name: "settings.ts", contents: after },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
interface DiffThemePreviewProps {
|
|
37
|
+
theme: DiffTheme;
|
|
38
|
+
fontFamily: string | null;
|
|
39
|
+
themeType: "dark" | "light";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A fixed diff rendered in the picked theme and font, in the app's current
|
|
44
|
+
* mode. Its own lazy chunk, like the diff viewers: it carries shiki.
|
|
45
|
+
*/
|
|
46
|
+
export default function DiffThemePreview({
|
|
47
|
+
theme,
|
|
48
|
+
fontFamily,
|
|
49
|
+
themeType,
|
|
50
|
+
}: DiffThemePreviewProps): ReactNode {
|
|
51
|
+
return (
|
|
52
|
+
<FileDiff
|
|
53
|
+
fileDiff={sample}
|
|
54
|
+
options={{
|
|
55
|
+
diffStyle: "unified",
|
|
56
|
+
theme: diffThemeNames(theme),
|
|
57
|
+
themeType,
|
|
58
|
+
overflow: "wrap",
|
|
59
|
+
unsafeCSS: `${diffFontCSS(fontFamily)} ${changeBackgroundCSS(theme)}`,
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type FileKind =
|
|
2
|
+
| "config"
|
|
3
|
+
| "css"
|
|
4
|
+
| "file"
|
|
5
|
+
| "html"
|
|
6
|
+
| "image"
|
|
7
|
+
| "javascript"
|
|
8
|
+
| "json"
|
|
9
|
+
| "jsx"
|
|
10
|
+
| "lock"
|
|
11
|
+
| "markdown"
|
|
12
|
+
| "package"
|
|
13
|
+
| "python"
|
|
14
|
+
| "rust"
|
|
15
|
+
| "shell"
|
|
16
|
+
| "sql"
|
|
17
|
+
| "svg"
|
|
18
|
+
| "text"
|
|
19
|
+
| "tsx"
|
|
20
|
+
| "typescript"
|
|
21
|
+
| "vue";
|
|
22
|
+
|
|
23
|
+
const lockFiles = new Set([
|
|
24
|
+
"bun.lock",
|
|
25
|
+
"bun.lockb",
|
|
26
|
+
"composer.lock",
|
|
27
|
+
"Gemfile.lock",
|
|
28
|
+
"package-lock.json",
|
|
29
|
+
"pnpm-lock.yaml",
|
|
30
|
+
"poetry.lock",
|
|
31
|
+
"yarn.lock",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const kindByExtension: Readonly<Record<string, FileKind>> = {
|
|
35
|
+
ts: "typescript",
|
|
36
|
+
mts: "typescript",
|
|
37
|
+
cts: "typescript",
|
|
38
|
+
tsx: "tsx",
|
|
39
|
+
js: "javascript",
|
|
40
|
+
mjs: "javascript",
|
|
41
|
+
cjs: "javascript",
|
|
42
|
+
jsx: "jsx",
|
|
43
|
+
json: "json",
|
|
44
|
+
jsonc: "json",
|
|
45
|
+
json5: "json",
|
|
46
|
+
md: "markdown",
|
|
47
|
+
mdx: "markdown",
|
|
48
|
+
css: "css",
|
|
49
|
+
scss: "css",
|
|
50
|
+
sass: "css",
|
|
51
|
+
less: "css",
|
|
52
|
+
html: "html",
|
|
53
|
+
htm: "html",
|
|
54
|
+
png: "image",
|
|
55
|
+
jpg: "image",
|
|
56
|
+
jpeg: "image",
|
|
57
|
+
gif: "image",
|
|
58
|
+
webp: "image",
|
|
59
|
+
avif: "image",
|
|
60
|
+
ico: "image",
|
|
61
|
+
svg: "svg",
|
|
62
|
+
yaml: "config",
|
|
63
|
+
yml: "config",
|
|
64
|
+
toml: "config",
|
|
65
|
+
ini: "config",
|
|
66
|
+
env: "config",
|
|
67
|
+
sh: "shell",
|
|
68
|
+
bash: "shell",
|
|
69
|
+
zsh: "shell",
|
|
70
|
+
fish: "shell",
|
|
71
|
+
sql: "sql",
|
|
72
|
+
py: "python",
|
|
73
|
+
rs: "rust",
|
|
74
|
+
vue: "vue",
|
|
75
|
+
txt: "text",
|
|
76
|
+
log: "text",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** What a file is, from its name, for the icon standing for it. */
|
|
80
|
+
export const fileKindOf = (path: string): FileKind => {
|
|
81
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
82
|
+
if (lockFiles.has(name) || name.endsWith(".lock")) return "lock";
|
|
83
|
+
if (name === "package.json") return "package";
|
|
84
|
+
const dot = name.lastIndexOf(".");
|
|
85
|
+
const known =
|
|
86
|
+
dot > 0 ? kindByExtension[name.slice(dot + 1).toLowerCase()] : undefined;
|
|
87
|
+
if (known !== undefined) return known;
|
|
88
|
+
// a dotfile without a known extension (.gitignore, .npmrc, .env)
|
|
89
|
+
if (name.startsWith(".")) return "config";
|
|
90
|
+
return "file";
|
|
91
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SVGIconElement } from "alouette";
|
|
2
|
+
import { Icon } from "alouette";
|
|
3
|
+
import { BracketsCurlyRegularIcon } from "alouette-icons/phosphor-icons/BracketsCurly";
|
|
4
|
+
import { FileRegularIcon } from "alouette-icons/phosphor-icons/File";
|
|
5
|
+
import { FileCssRegularIcon } from "alouette-icons/phosphor-icons/FileCss";
|
|
6
|
+
import { FileHtmlRegularIcon } from "alouette-icons/phosphor-icons/FileHtml";
|
|
7
|
+
import { FileImageRegularIcon } from "alouette-icons/phosphor-icons/FileImage";
|
|
8
|
+
import { FileJsRegularIcon } from "alouette-icons/phosphor-icons/FileJs";
|
|
9
|
+
import { FileJsxRegularIcon } from "alouette-icons/phosphor-icons/FileJsx";
|
|
10
|
+
import { FileLockRegularIcon } from "alouette-icons/phosphor-icons/FileLock";
|
|
11
|
+
import { FileMdRegularIcon } from "alouette-icons/phosphor-icons/FileMd";
|
|
12
|
+
import { FilePyRegularIcon } from "alouette-icons/phosphor-icons/FilePy";
|
|
13
|
+
import { FileRsRegularIcon } from "alouette-icons/phosphor-icons/FileRs";
|
|
14
|
+
import { FileSqlRegularIcon } from "alouette-icons/phosphor-icons/FileSql";
|
|
15
|
+
import { FileSvgRegularIcon } from "alouette-icons/phosphor-icons/FileSvg";
|
|
16
|
+
import { FileTsRegularIcon } from "alouette-icons/phosphor-icons/FileTs";
|
|
17
|
+
import { FileTsxRegularIcon } from "alouette-icons/phosphor-icons/FileTsx";
|
|
18
|
+
import { FileTxtRegularIcon } from "alouette-icons/phosphor-icons/FileTxt";
|
|
19
|
+
import { FileVueRegularIcon } from "alouette-icons/phosphor-icons/FileVue";
|
|
20
|
+
import { GearSixRegularIcon } from "alouette-icons/phosphor-icons/GearSix";
|
|
21
|
+
import { PackageRegularIcon } from "alouette-icons/phosphor-icons/Package";
|
|
22
|
+
import { TerminalRegularIcon } from "alouette-icons/phosphor-icons/Terminal";
|
|
23
|
+
import { TestTubeRegularIcon } from "alouette-icons/phosphor-icons/TestTube";
|
|
24
|
+
import type { ReactNode } from "react";
|
|
25
|
+
import type { FileKind } from "./file-kind.ts";
|
|
26
|
+
import { fileKindOf } from "./file-kind.ts";
|
|
27
|
+
|
|
28
|
+
const glyphOf = (kind: FileKind): SVGIconElement => {
|
|
29
|
+
if (kind === "typescript") return <FileTsRegularIcon />;
|
|
30
|
+
if (kind === "tsx") return <FileTsxRegularIcon />;
|
|
31
|
+
if (kind === "javascript") return <FileJsRegularIcon />;
|
|
32
|
+
if (kind === "jsx") return <FileJsxRegularIcon />;
|
|
33
|
+
if (kind === "json") return <BracketsCurlyRegularIcon />;
|
|
34
|
+
if (kind === "package") return <PackageRegularIcon />;
|
|
35
|
+
if (kind === "lock") return <FileLockRegularIcon />;
|
|
36
|
+
if (kind === "markdown") return <FileMdRegularIcon />;
|
|
37
|
+
if (kind === "css") return <FileCssRegularIcon />;
|
|
38
|
+
if (kind === "html") return <FileHtmlRegularIcon />;
|
|
39
|
+
if (kind === "image") return <FileImageRegularIcon />;
|
|
40
|
+
if (kind === "svg") return <FileSvgRegularIcon />;
|
|
41
|
+
if (kind === "config") return <GearSixRegularIcon />;
|
|
42
|
+
if (kind === "shell") return <TerminalRegularIcon />;
|
|
43
|
+
if (kind === "sql") return <FileSqlRegularIcon />;
|
|
44
|
+
if (kind === "python") return <FilePyRegularIcon />;
|
|
45
|
+
if (kind === "rust") return <FileRsRegularIcon />;
|
|
46
|
+
if (kind === "vue") return <FileVueRegularIcon />;
|
|
47
|
+
if (kind === "text") return <FileTxtRegularIcon />;
|
|
48
|
+
return <FileRegularIcon />;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
interface FileTypeIconProps {
|
|
52
|
+
path: string;
|
|
53
|
+
/** a test or a story, whatever its extension */
|
|
54
|
+
test: boolean;
|
|
55
|
+
size: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function FileTypeIcon({
|
|
59
|
+
path,
|
|
60
|
+
test,
|
|
61
|
+
size,
|
|
62
|
+
}: FileTypeIconProps): ReactNode {
|
|
63
|
+
return (
|
|
64
|
+
<Icon
|
|
65
|
+
icon={test ? <TestTubeRegularIcon /> : glyphOf(fileKindOf(path))}
|
|
66
|
+
size={size}
|
|
67
|
+
className="text-muted"
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -53,7 +53,7 @@ const submitBlocker = (
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* "Review changes", ending the
|
|
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
|
|
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
|
-
{`${
|
|
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,
|
|
@@ -51,11 +51,14 @@ export interface ReviewDraftActions {
|
|
|
51
51
|
remove: (key: string) => void;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Raised off the code on its own highlight ground, not the line's: a selected
|
|
56
|
+
* line's tint would show through, and a hairline left it reading as code.
|
|
57
|
+
*/
|
|
55
58
|
function Frame({ children }: { children: ReactNode }): ReactNode {
|
|
56
59
|
return (
|
|
57
60
|
<View className="p-xs">
|
|
58
|
-
<View className="min-w-0 gap-sm rounded-sm
|
|
61
|
+
<View className="min-w-0 gap-sm rounded-sm bg-highlight p-m shadow-s">
|
|
59
62
|
{children}
|
|
60
63
|
</View>
|
|
61
64
|
</View>
|
|
@@ -114,7 +117,7 @@ export function ThreadAnnotation({
|
|
|
114
117
|
Resolved
|
|
115
118
|
</Badge>
|
|
116
119
|
<Text className="text-sm text-muted">
|
|
117
|
-
{`${first?.author.login ?? "thread"}, ${
|
|
120
|
+
{`${first?.author.login ?? "thread"}, ${pluralOf(thread.commentsTotal, "comment")}`}
|
|
118
121
|
</Text>
|
|
119
122
|
<Button
|
|
120
123
|
size="sm"
|
|
@@ -160,7 +163,7 @@ export function ThreadAnnotation({
|
|
|
160
163
|
</SuggestionOriginalContext>
|
|
161
164
|
{unlisted > 0 ? (
|
|
162
165
|
<Text className="text-xs text-muted">
|
|
163
|
-
{`${
|
|
166
|
+
{`${pluralOf(unlisted, "more comment")} on GitHub`}
|
|
164
167
|
</Text>
|
|
165
168
|
) : null}
|
|
166
169
|
{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
|
|
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
|
[
|