@reviewflow/ui 6.25.0 → 6.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reviewflow/ui",
3
- "version": "6.25.0",
3
+ "version": "6.27.0",
4
4
  "description": "react-native pull request screen components. Consumed as SOURCES (no build): exports resolve `default` to ./src/index.ts, which metro transforms like the app's own files; the consuming app's css must @source this package's src so tailwind keeps its classes.",
5
5
  "keywords": [
6
6
  "alouette",
@@ -42,6 +42,10 @@
42
42
  "types": "./src/index.ts",
43
43
  "default": "./src/index.ts"
44
44
  },
45
+ "./diff-viewer": {
46
+ "types": "./src/components/diff-viewer.tsx",
47
+ "default": "./src/components/diff-viewer.tsx"
48
+ },
45
49
  "./diff-worker-pool": {
46
50
  "types": "./src/components/branch-diff/diff-worker-pool.tsx",
47
51
  "browser": "./src/components/branch-diff/diff-worker-pool.web.tsx",
@@ -73,7 +77,7 @@
73
77
  "@lezer/markdown": "1.7.2",
74
78
  "@pierre/diffs": "1.5.2",
75
79
  "@pierre/trees": "1.0.0-beta.6",
76
- "@reviewflow/sdk": "6.25.0",
80
+ "@reviewflow/sdk": "6.27.0",
77
81
  "@tanstack/markdown": "0.0.13",
78
82
  "alouette-icons": "12.0.0",
79
83
  "gemoji": "8.1.0",
@@ -31,7 +31,49 @@ const stopRowPress = (event: GestureResponderEvent): void => {
31
31
  event.stopPropagation();
32
32
  };
33
33
 
34
- const metaSeparator = <Text className="text-xs text-muted">·</Text>;
34
+ export function PullRequestMetaSeparator(): ReactNode {
35
+ return <Text className="text-xs text-muted">·</Text>;
36
+ }
37
+
38
+ const metaSeparator = <PullRequestMetaSeparator />;
39
+
40
+ export interface PullRequestRowFrameProps {
41
+ ariaLabel: string;
42
+ onPress: () => void;
43
+ /** the meta line's items, joined with `PullRequestMetaSeparator` */
44
+ meta: ReactNode;
45
+ title: string;
46
+ /** beside the title, e.g. who it waits on */
47
+ titleAside?: ReactNode;
48
+ /** under the title */
49
+ children?: ReactNode;
50
+ }
51
+
52
+ /**
53
+ * The layout every pull request row shares, whatever its source: a meta line,
54
+ * the title, then what the source adds.
55
+ */
56
+ export function PullRequestRowFrame({
57
+ ariaLabel,
58
+ onPress,
59
+ meta,
60
+ title,
61
+ titleAside,
62
+ children,
63
+ }: PullRequestRowFrameProps): ReactNode {
64
+ return (
65
+ <PressableListItem role="link" aria-label={ariaLabel} onPress={onPress}>
66
+ <View className="min-w-0 gap-xxs">
67
+ <View className="flex-row flex-wrap items-center gap-xs">{meta}</View>
68
+ <View className="flex-row flex-wrap items-baseline gap-xs">
69
+ <Text className="font-body-bold">{title}</Text>
70
+ {titleAside}
71
+ </View>
72
+ {children}
73
+ </View>
74
+ </PressableListItem>
75
+ );
76
+ }
35
77
 
36
78
  const statusSeparator = <Text className="text-sm text-muted">·</Text>;
37
79
 
@@ -59,7 +101,7 @@ function MetaLine({ pr }: { pr: ReviewflowBoardPr }): ReactNode {
59
101
  const flowDate = reviewflowPrFlowDate(pr);
60
102
 
61
103
  return (
62
- <View className="flex-row flex-wrap items-center gap-xs">
104
+ <>
63
105
  <Text className="font-mono text-xs text-muted">
64
106
  {`${pr.ownerRepo}#${String(pr.number)}`}
65
107
  </Text>
@@ -95,7 +137,7 @@ function MetaLine({ pr }: { pr: ReviewflowBoardPr }): ReactNode {
95
137
  <Text className="text-xs text-muted">{flowDate}</Text>
96
138
  </>
97
139
  )}
98
- </View>
140
+ </>
99
141
  );
100
142
  }
101
143
 
@@ -161,24 +203,21 @@ export function PullRequestRow({
161
203
  const extra = renderRowExtra?.(row);
162
204
 
163
205
  return (
164
- <PressableListItem
165
- role="link"
166
- aria-label={`Open ${pr.ownerRepo}#${String(pr.number)} ${pr.title}`}
206
+ <PullRequestRowFrame
207
+ ariaLabel={`Open ${pr.ownerRepo}#${String(pr.number)} ${pr.title}`}
208
+ meta={<MetaLine pr={pr} />}
209
+ title={pr.title}
210
+ titleAside={
211
+ owners === null ? null : (
212
+ <Text className="text-sm text-muted">{owners}</Text>
213
+ )
214
+ }
167
215
  onPress={() => {
168
216
  onOpenPullRequest({ ownerRepo: pr.ownerRepo, number: pr.number });
169
217
  }}
170
218
  >
171
- <View className="min-w-0 gap-xxs">
172
- <MetaLine pr={pr} />
173
- <View className="flex-row flex-wrap items-baseline gap-xs">
174
- <Text className="font-body-bold">{pr.title}</Text>
175
- {owners === null ? null : (
176
- <Text className="text-sm text-muted">{owners}</Text>
177
- )}
178
- </View>
179
- <StatusLine pr={pr} context={context} />
180
- {extra === undefined || extra === null ? null : extra}
181
- </View>
182
- </PressableListItem>
219
+ <StatusLine pr={pr} context={context} />
220
+ {extra === undefined || extra === null ? null : extra}
221
+ </PullRequestRowFrame>
183
222
  );
184
223
  }
@@ -0,0 +1,86 @@
1
+ import type { FileDiffMetadata } from "@pierre/diffs";
2
+ import type { Accent, SVGIconElement } from "alouette";
3
+ import { AccentScope, Icon, Tooltip, View } from "alouette";
4
+ import { ArrowSquareRightRegularIcon } from "alouette-icons/phosphor-icons/ArrowSquareRight";
5
+ import { MinusSquareRegularIcon } from "alouette-icons/phosphor-icons/MinusSquare";
6
+ import { PencilSimpleLineRegularIcon } from "alouette-icons/phosphor-icons/PencilSimpleLine";
7
+ import { PlusSquareRegularIcon } from "alouette-icons/phosphor-icons/PlusSquare";
8
+ import type { ReactNode } from "react";
9
+
10
+ /**
11
+ * For a pierre file's `unsafeCSS`, with `DiffChangeMark` in its header prefix:
12
+ * pierre draws its change icon inside the file's shadow root, where nothing of
13
+ * ours can give it a tooltip.
14
+ */
15
+ export const hidePierreChangeIcon = "[data-change-icon] { display: none; }";
16
+
17
+ interface ChangeMarkProps {
18
+ label: string;
19
+ accent: Accent;
20
+ icon: SVGIconElement;
21
+ }
22
+
23
+ function ChangeMark({ label, accent, icon }: ChangeMarkProps): ReactNode {
24
+ return (
25
+ <Tooltip
26
+ content={label}
27
+ render={(triggerProps) => (
28
+ <View role="img" aria-label={label} {...triggerProps}>
29
+ <AccentScope accent={accent}>
30
+ <Icon icon={icon} size={16} className="text-accent" />
31
+ </AccentScope>
32
+ </View>
33
+ )}
34
+ />
35
+ );
36
+ }
37
+
38
+ interface DiffChangeMarkProps {
39
+ type: FileDiffMetadata["type"];
40
+ }
41
+
42
+ export function DiffChangeMark({ type }: DiffChangeMarkProps): ReactNode {
43
+ if (type === "new") {
44
+ return (
45
+ <ChangeMark
46
+ label="Added"
47
+ accent="success"
48
+ icon={<PlusSquareRegularIcon />}
49
+ />
50
+ );
51
+ }
52
+ if (type === "deleted") {
53
+ return (
54
+ <ChangeMark
55
+ label="Deleted"
56
+ accent="danger"
57
+ icon={<MinusSquareRegularIcon />}
58
+ />
59
+ );
60
+ }
61
+ if (type === "rename-pure") {
62
+ return (
63
+ <ChangeMark
64
+ label="Renamed, content unchanged"
65
+ accent="warning"
66
+ icon={<ArrowSquareRightRegularIcon />}
67
+ />
68
+ );
69
+ }
70
+ if (type === "rename-changed") {
71
+ return (
72
+ <ChangeMark
73
+ label="Renamed and modified"
74
+ accent="warning"
75
+ icon={<ArrowSquareRightRegularIcon />}
76
+ />
77
+ );
78
+ }
79
+ return (
80
+ <ChangeMark
81
+ label="Modified"
82
+ accent="info"
83
+ icon={<PencilSimpleLineRegularIcon />}
84
+ />
85
+ );
86
+ }
@@ -18,6 +18,9 @@ interface CollapsedState {
18
18
  * not the file list: the viewer parses the patch and is loaded lazily. Stored
19
19
  * as a default plus exceptions, so either "all" needs no list of files and a
20
20
  * file new to a refreshed diff follows the last "all" pressed.
21
+ *
22
+ * @deprecated `DiffViewer` and the pull request diff take a
23
+ * `PullRequestCollapse`: use `usePullRequestCollapse`.
21
24
  */
22
25
  export const useCollapsedFiles = (): CollapsedFiles => {
23
26
  const [state, setState] = useState<CollapsedState>({
@@ -0,0 +1,76 @@
1
+ import type { CheckoutStatus } from "@reviewflow/sdk";
2
+ import { pullRefusal, pushRefusal } from "@reviewflow/sdk";
3
+ import { ActionButton } from "alouette";
4
+ import { CloudArrowDownRegularIcon } from "alouette-icons/phosphor-icons/CloudArrowDown";
5
+ import { CloudArrowUpRegularIcon } from "alouette-icons/phosphor-icons/CloudArrowUp";
6
+ import type { ReactNode } from "react";
7
+ import { errorToMessage } from "../services/errorToMessage.ts";
8
+
9
+ export interface CheckoutSyncButtonsProps {
10
+ status: CheckoutStatus;
11
+ hasOrigin: boolean;
12
+ /** what the user calls this checkout, so each label names one of several */
13
+ name: string;
14
+ onPull: () => Promise<void>;
15
+ onPush: () => Promise<void>;
16
+ /** after every press, succeeded or not: git state moved either way, or may have */
17
+ onDone: () => void;
18
+ }
19
+
20
+ /**
21
+ * Pull and Push appear only when `pullRefusal` / `pushRefusal` would not refuse
22
+ * them — the server asks the same question before running git, so a button is
23
+ * never offered for a press the server turns down on its own terms. A failure
24
+ * is git's reason, shown under the button.
25
+ */
26
+ export function CheckoutSyncButtons({
27
+ status,
28
+ hasOrigin,
29
+ name,
30
+ onPull,
31
+ onPush,
32
+ onDone,
33
+ }: CheckoutSyncButtonsProps): ReactNode {
34
+ const run = async (action: () => Promise<void>): Promise<void> => {
35
+ try {
36
+ await action();
37
+ } finally {
38
+ onDone();
39
+ }
40
+ };
41
+
42
+ return (
43
+ <>
44
+ {pullRefusal(status) === null ? (
45
+ <ActionButton
46
+ text={`Pull ${String(status.behind)}`}
47
+ icon={<CloudArrowDownRegularIcon />}
48
+ accent="brand"
49
+ variant="tonal"
50
+ size="sm"
51
+ aria-label={`Pull ${name}`}
52
+ errorMessageVariant="flat"
53
+ errorToMessage={errorToMessage}
54
+ onPress={() => run(onPull)}
55
+ />
56
+ ) : null}
57
+ {pushRefusal(status, hasOrigin) === null ? (
58
+ <ActionButton
59
+ text={
60
+ status.upstream === null
61
+ ? "Publish"
62
+ : `Push ${String(status.ahead)}`
63
+ }
64
+ icon={<CloudArrowUpRegularIcon />}
65
+ accent="brand"
66
+ variant="tonal"
67
+ size="sm"
68
+ aria-label={`Push ${name}`}
69
+ errorMessageVariant="flat"
70
+ errorToMessage={errorToMessage}
71
+ onPress={() => run(onPush)}
72
+ />
73
+ ) : null}
74
+ </>
75
+ );
76
+ }