| 1 | import type { ReactNode } from "react"; |
| 2 | import { Pressable, View } from "react-native"; |
| 3 | import { AppText as Text } from "../../components/AppText"; |
| 4 | import { SymbolView } from "../../components/AppSymbol"; |
| 5 | import { cn } from "../../lib/cn"; |
| 6 | import type { ModelOption } from "../../lib/modelOptions"; |
| 7 | |
| 8 | export type ModelRowProps = { |
| 9 | readonly option: ModelOption; |
| 10 | readonly selected: boolean; |
| 11 | readonly onPress: () => void; |
| 12 | readonly isFavorite: boolean; |
| 13 | readonly favoritesLoaded: boolean; |
| 14 | readonly onToggleFavorite: () => void; |
| 15 | readonly onToggleHidden?: () => void; |
| 16 | readonly hiddenUpdatePending?: boolean; |
| 17 | readonly isFirst: boolean; |
| 18 | readonly isLast: boolean; |
| 19 | }; |
| 20 | |
| 21 | export type ChoiceRowProps = { |
| 22 | readonly label: string; |
| 23 | readonly description?: string; |
| 24 | readonly selected: boolean; |
| 25 | readonly onPress: () => void; |
| 26 | readonly isLast: boolean; |
| 27 | }; |
| 28 | |
| 29 | type RowSelectionProps = { |
| 30 | readonly leadingSelection?: ReactNode; |
| 31 | readonly trailingSelection?: ReactNode; |
| 32 | readonly minimumHeight?: number; |
| 33 | }; |
| 34 | |
| 35 | export function ModelRowContent( |
| 36 | props: ModelRowProps & |
| 37 | RowSelectionProps & { |
| 38 | readonly labelNumberOfLines: number; |
| 39 | readonly selectedClassName?: string; |
| 40 | }, |
| 41 | ) { |
| 42 | return ( |
| 43 | <View |
| 44 | style={props.minimumHeight === undefined ? undefined : { minHeight: props.minimumHeight }} |
| 45 | className={cn( |
| 46 | "mx-4 min-h-11 flex-row items-center gap-2 bg-grouped-card px-4", |
| 47 | props.selectedClassName, |
| 48 | props.isFirst && "rounded-t-2xl", |
| 49 | props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle", |
| 50 | )} |
| 51 | > |
| 52 | <Pressable |
| 53 | accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")} |
| 54 | accessibilityRole="radio" |
| 55 | accessibilityState={{ |
| 56 | checked: props.selected, |
| 57 | disabled: props.option.isUnavailable === true || props.option.isHidden === true, |
| 58 | }} |
| 59 | className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2 active:opacity-70" |
| 60 | disabled={props.option.isUnavailable || props.option.isHidden} |
| 61 | onPress={props.onPress} |
| 62 | > |
| 63 | {props.leadingSelection} |
| 64 | <View className="min-w-0 flex-1"> |
| 65 | <View className="flex-row items-center gap-2"> |
| 66 | <Text |
| 67 | className="min-w-0 shrink text-base font-t3-medium text-foreground" |
| 68 | numberOfLines={props.labelNumberOfLines} |
| 69 | > |
| 70 | {props.option.label} |
| 71 | </Text> |
| 72 | {props.option.isDefault ? ( |
| 73 | <View className="rounded-md bg-subtle-strong px-1.5 py-0.5"> |
| 74 | <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text> |
| 75 | </View> |
| 76 | ) : null} |
| 77 | {props.option.isLegacy ? ( |
| 78 | <View className="rounded-md bg-subtle px-1.5 py-0.5"> |
| 79 | <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text> |
| 80 | </View> |
| 81 | ) : null} |
| 82 | {props.option.isHidden ? ( |
| 83 | <Text className="text-xs text-foreground-muted">Hidden</Text> |
| 84 | ) : null} |
| 85 | {props.option.isUnavailable ? ( |
| 86 | <Text className="text-xs text-foreground">Unavailable</Text> |
| 87 | ) : null} |
| 88 | </View> |
| 89 | {props.option.subtitle ? ( |
| 90 | <Text |
| 91 | className="text-xs text-foreground-muted" |
| 92 | numberOfLines={props.labelNumberOfLines} |
| 93 | > |
| 94 | {props.option.subtitle} |
| 95 | </Text> |
| 96 | ) : null} |
| 97 | </View> |
| 98 | {props.trailingSelection} |
| 99 | </Pressable> |
| 100 | {props.onToggleHidden ? ( |
| 101 | <Pressable |
| 102 | accessibilityLabel={`${props.option.isHidden ? "Show" : "Hide"} model: ${props.option.providerLabel}, ${props.option.label}`} |
| 103 | accessibilityRole="button" |
| 104 | accessibilityState={{ disabled: props.hiddenUpdatePending === true }} |
| 105 | className="min-h-11 min-w-11 items-center justify-center" |
| 106 | disabled={props.hiddenUpdatePending} |
| 107 | onPress={props.onToggleHidden} |
| 108 | > |
| 109 | <SymbolView |
| 110 | name={props.option.isHidden ? "eye.slash" : "eye"} |
| 111 | size={18} |
| 112 | tintColorClassName={props.option.isHidden ? "accent-icon-subtle" : "accent-icon"} |
| 113 | type="monochrome" |
| 114 | /> |
| 115 | </Pressable> |
| 116 | ) : null} |
| 117 | <Pressable |
| 118 | accessibilityLabel={`${props.isFavorite ? "Remove from" : "Add to"} favorites: ${ |
| 119 | props.option.providerLabel |
| 120 | }, ${props.option.label}`} |
| 121 | accessibilityRole="button" |
| 122 | accessibilityState={{ disabled: !props.favoritesLoaded, selected: props.isFavorite }} |
| 123 | className="min-h-11 min-w-11 items-center justify-center" |
| 124 | disabled={!props.favoritesLoaded} |
| 125 | onPress={props.onToggleFavorite} |
| 126 | > |
| 127 | <SymbolView |
| 128 | name={props.isFavorite ? "star.fill" : "star"} |
| 129 | size={18} |
| 130 | tintColorClassName={props.isFavorite ? "accent-icon" : "accent-icon-subtle"} |
| 131 | type="monochrome" |
| 132 | /> |
| 133 | </Pressable> |
| 134 | </View> |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | /** Single option inside a submenu panel. */ |
| 139 | export function ChoiceRowContent(props: ChoiceRowProps & RowSelectionProps) { |
| 140 | return ( |
| 141 | <Pressable |
| 142 | accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label} |
| 143 | accessibilityRole="radio" |
| 144 | accessibilityState={{ checked: props.selected }} |
| 145 | onPress={props.onPress} |
| 146 | style={props.minimumHeight === undefined ? undefined : { minHeight: props.minimumHeight }} |
| 147 | className={cn( |
| 148 | "min-h-14 flex-row items-center gap-3 bg-grouped-card px-4 py-3 active:bg-subtle", |
| 149 | !props.isLast && "border-b border-border-subtle", |
| 150 | )} |
| 151 | > |
| 152 | {props.leadingSelection} |
| 153 | <View className="min-w-0 flex-1 gap-0.5"> |
| 154 | <Text className="text-base font-t3-medium text-foreground">{props.label}</Text> |
| 155 | {props.description ? ( |
| 156 | <Text className="text-sm leading-5 text-foreground-muted">{props.description}</Text> |
| 157 | ) : null} |
| 158 | </View> |
| 159 | {props.trailingSelection} |
| 160 | </Pressable> |
| 161 | ); |
| 162 | } |