1import { SymbolView } from "../../../components/AppSymbol";
2import type { ComponentProps } from "react";
3import { Platform, Pressable, View } from "react-native";
4import { AppText as Text } from "../../../components/AppText";
5import { MaterialButton } from "../../../components/MaterialButton";
6import { cn } from "../../../lib/cn";
7
8/* ─── Shared sheet components ──────────────────────────────────────── */
9
10export function SheetActionButton(props: {
11 readonly icon: ComponentProps<typeof SymbolView>["name"];
12 readonly label: string;
13 readonly disabled?: boolean;
14 readonly tone?: "primary" | "secondary" | "danger";
15 readonly onPress: () => void;
16}) {
17 if (Platform.OS === "android")
18 return (
19 <View className="flex-1">
20 <MaterialButton
21 label={props.label}
22 tone={props.tone}
23 disabled={props.disabled}
24 onPress={props.onPress}
25 fullWidth
26 />
27 </View>
28 );
29 const tone = props.tone ?? "secondary";
30 const textColorClassName =
31 tone === "primary"
32 ? "accent-primary-foreground"
33 : tone === "danger"
34 ? "accent-danger-foreground"
35 : "accent-secondary-foreground";
36
37 return (
38 <Pressable
39 className={cn(
40 "min-h-[48px] flex-row items-center justify-center gap-2 px-4 py-3 disabled:opacity-[0.45]",
41 "flex-1 rounded-[18px]",
42 tone === "primary" ? "bg-primary" : tone === "danger" ? "bg-danger" : "bg-secondary",
43 tone !== "primary" &&
44 (tone === "danger" ? "border border-danger-border" : "border border-secondary-border"),
45 )}
46 accessibilityRole="button"
47 disabled={props.disabled}
48 onPress={props.onPress}
49 >
50 <SymbolView
51 name={props.icon}
52 size={16}
53 tintColorClassName={textColorClassName}
54 type="monochrome"
55 />
56 <Text
57 className={cn(
58 "text-xs font-t3-bold tracking-[0.9px] uppercase",
59 tone === "primary"
60 ? "text-primary-foreground"
61 : tone === "danger"
62 ? "text-danger-foreground"
63 : "text-secondary-foreground",
64 )}
65 >
66 {props.label}
67 </Text>
68 </Pressable>
69 );
70}
71
72export function MetaCard(props: { readonly label: string; readonly value: string }) {
73 return (
74 <View className="bg-card px-4 py-3 android:rounded-[20px] ios:rounded-[18px] ios:border ios:border-border">
75 <Text className="text-foreground-muted text-2xs font-t3-bold tracking-[0.9px] uppercase">
76 {props.label}
77 </Text>
78 <Text selectable className="text-foreground text-sm font-medium" numberOfLines={1}>
79 {props.value}
80 </Text>
81 </View>
82 );
83}
84
85export function SheetListRow(props: {
86 readonly icon: ComponentProps<typeof SymbolView>["name"];
87 readonly title: string;
88 readonly subtitle?: string | null;
89 readonly disabled?: boolean;
90 readonly onPress: () => void;
91}) {
92 return (
93 <Pressable
94 className="flex-row items-center py-3 disabled:opacity-[0.45] android:min-h-16 android:gap-4 android:px-4 android:active:bg-subtle ios:gap-3 ios:px-1"
95 disabled={props.disabled}
96 onPress={props.onPress}
97 >
98 <View className="items-center justify-center android:size-6 ios:bg-subtle ios:h-9 ios:w-9 ios:rounded-full">
99 <SymbolView
100 name={props.icon}
101 size={Platform.OS === "android" ? 24 : 16}
102 tintColorClassName="accent-icon"
103 type="monochrome"
104 />
105 </View>
106 <View className="flex-1 gap-0.5">
107 <Text className="text-foreground text-base android:font-t3-medium ios:font-t3-bold">
108 {props.title}
109 </Text>
110 {props.subtitle ? (
111 <Text className="text-foreground-muted text-xs leading-snug">{props.subtitle}</Text>
112 ) : null}
113 </View>
114 {Platform.OS !== "android" ? (
115 <SymbolView
116 name="chevron.right"
117 size={13}
118 tintColorClassName="accent-icon-subtle"
119 type="monochrome"
120 />
121 ) : null}
122 </Pressable>
123 );
124}
125
126/* ─── Shared utilities ──────────────────────────────────────────────── */
127
128export function menuItemIconName(
129 icon: "commit" | "push" | "pr",
130): ComponentProps<typeof SymbolView>["name"] {
131 if (icon === "commit") return "checkmark.circle";
132 if (icon === "push") return "arrow.up.circle";
133 return "arrow.up.right.circle";
134}
135
136export function statusSummary(
137 gitStatus: {
138 readonly isRepo?: boolean;
139 readonly hasWorkingTreeChanges?: boolean;
140 readonly workingTree?: { readonly files: readonly { readonly path: string }[] };
141 readonly aheadCount?: number;
142 readonly behindCount?: number;
143 readonly pr?: { readonly state?: string; readonly number?: number } | null;
144 } | null,
145): string {
146 if (!gitStatus) {
147 return "Loading branch status\u2026";
148 }
149
150 if (!gitStatus.isRepo) {
151 return "Not a git repository";
152 }
153
154 const parts: string[] = [];
155 if (gitStatus.hasWorkingTreeChanges) {
156 const fileCount = gitStatus.workingTree?.files.length ?? 0;
157 parts.push(`${fileCount} file${fileCount === 1 ? "" : "s"} changed`);
158 } else {
159 parts.push("Clean");
160 }
161 if ((gitStatus.aheadCount ?? 0) > 0) {
162 parts.push(`${gitStatus.aheadCount} ahead`);
163 }
164 if ((gitStatus.behindCount ?? 0) > 0) {
165 parts.push(`${gitStatus.behindCount} behind`);
166 }
167 if (gitStatus.pr?.state === "open") {
168 parts.push(`PR #${gitStatus.pr.number} open`);
169 }
170
171 return parts.join(" \u00b7 ");
172}