1import type { PullRequestReviewGuide } from "@t3tools/contracts";
2import { stopFilePaths } from "@t3tools/client-runtime/review-guide-tour";
3import { Pressable, View } from "react-native";
4import { AppText as Text } from "../../../components/AppText";
5import { SymbolView } from "../../../components/AppSymbol";
6import { PullRequestMarkdown } from "../PullRequestMarkdown";
7import { ControlPillMenu } from "../../../components/ControlPillMenu";
8
9import { useAppearancePreferences } from "../../settings/appearance/AppearancePreferencesProvider";
10
11/** The stops are a reading sequence, with file context kept beside each idea. */
12export function GuideOutline({
13 guide,
14 onOpen,
15 onRewrite,
16}: {
17 readonly guide: PullRequestReviewGuide;
18 readonly onOpen: (position: number) => void;
19 readonly onRewrite?: () => void;
20}) {
21 const { appearance, setCodeWordBreak } = useAppearancePreferences();
22 const files = new Set(guide.stops.flatMap(stopFilePaths));
23 return (
24 <View>
25 <View className="mb-5 flex-row items-center justify-between">
26 <View className="flex-row items-baseline gap-2">
27 <Text className="text-base font-t3-semibold">{guide.stops.length} stops</Text>
28 <Text className="text-sm text-foreground-muted">
29 through {files.size} {files.size === 1 ? "file" : "files"}
30 </Text>
31 </View>
32 <ControlPillMenu
33 title="Guide options"
34 actions={[
35 {
36 id: "word-wrap",
37 title: "Word wrap",
38 image: "text.alignleft",
39 state: appearance.codeWordBreak ? "on" : "off",
40 },
41 ...(onRewrite
42 ? [{ id: "rewrite", title: "Rewrite guide", image: "arrow.clockwise" }]
43 : []),
44 ]}
45 onPressAction={({ nativeEvent }) =>
46 nativeEvent.event === "word-wrap"
47 ? setCodeWordBreak(!appearance.codeWordBreak)
48 : onRewrite?.()
49 }
50 >
51 <Pressable
52 accessibilityRole="button"
53 accessibilityLabel="Guide options"
54 className="h-11 w-11 items-center justify-center rounded-full active:bg-grouped-card"
55 >
56 <SymbolView name="ellipsis" size={20} tintColorClassName="accent-icon-muted" />
57 </Pressable>
58 </ControlPillMenu>
59 </View>
60 {guide.stops[0]?.summary ? (
61 <View className="mb-6">
62 <PullRequestMarkdown markdown={guide.stops[0].summary} />
63 </View>
64 ) : null}
65 {guide.stops.map((stop, position) => {
66 const paths = stopFilePaths(stop);
67 const firstPath = paths[0];
68 const filename = firstPath?.split("/").at(-1);
69 return (
70 <Pressable
71 key={`${guide.headSha}:${JSON.stringify(stop)}`}
72 accessibilityRole="button"
73 accessibilityLabel={`Stop ${position + 1}: ${stop.title}`}
74 onPress={() => onOpen(position)}
75 className="flex-row gap-4 active:opacity-60"
76 >
77 <View className="w-8 items-center">
78 <View className="h-8 w-8 items-center justify-center rounded-full border border-border bg-grouped-card">
79 <Text className="text-sm font-t3-medium tabular-nums">{position + 1}</Text>
80 </View>
81 {position < guide.stops.length - 1 ? (
82 <View className="min-h-5 w-px flex-1 bg-border-subtle" />
83 ) : null}
84 </View>
85 <View className="min-w-0 flex-1 gap-2 pb-7 pt-0.5">
86 <Text className="text-base leading-6 font-t3-medium">{stop.title}</Text>
87 <View className="flex-row items-center gap-1.5">
88 {stop.attention ? (
89 <SymbolView name="eye" size={14} tintColorClassName="accent-warning-foreground" />
90 ) : null}
91 <Text className="shrink text-xs text-foreground-muted" numberOfLines={1}>
92 {filename ?? "Context"}
93 {paths.length > 1 ? ` +${paths.length - 1}` : ""}
94 </Text>
95 </View>
96 </View>
97 <View className="pt-2">
98 <SymbolView name="chevron.right" size={12} tintColorClassName="accent-icon-muted" />
99 </View>
100 </Pressable>
101 );
102 })}
103 {guide.stops.length ? (
104 <Pressable
105 accessibilityRole="button"
106 onPress={() => onOpen(0)}
107 className="min-h-11 flex-row items-center justify-end gap-2 active:opacity-60"
108 >
109 <Text className="text-sm font-t3-medium">Start tour</Text>
110 <SymbolView name="arrow.right" size={17} tintColorClassName="accent-foreground" />
111 </Pressable>
112 ) : null}
113 </View>
114 );
115}