| 1 | import { SymbolView } from "../../../components/AppSymbol"; |
| 2 | import type { ComponentProps } from "react"; |
| 3 | import { Platform, Pressable, View } from "react-native"; |
| 4 | import { AppText as Text } from "../../../components/AppText"; |
| 5 | import { MaterialButton } from "../../../components/MaterialButton"; |
| 6 | import { cn } from "../../../lib/cn"; |
| 7 | |
| 8 | /* ─── Shared sheet components ──────────────────────────────────────── */ |
| 9 | |
| 10 | export 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 | |
| 72 | export 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 | |
| 85 | export 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 | |
| 128 | export 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 | |
| 136 | export 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 | } |