Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at readme-update 320 lines 7.4 kB view raw
1import {createContext, useContext, useMemo} from 'react' 2import { 3 type GestureResponderEvent, 4 type StyleProp, 5 View, 6 type ViewStyle, 7} from 'react-native' 8 9import {HITSLOP_10} from '#/lib/constants' 10import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' 11import * as Button from '#/components/Button' 12import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' 13import {Link, type LinkProps} from '#/components/Link' 14import {createPortalGroup} from '#/components/Portal' 15import {Text} from '#/components/Typography' 16 17const ItemContext = createContext({ 18 destructive: false, 19 withinGroup: false, 20}) 21ItemContext.displayName = 'SettingsListItemContext' 22 23const Portal = createPortalGroup() 24 25export function Container({children}: {children: React.ReactNode}) { 26 return <View style={[a.flex_1, a.py_md]}>{children}</View> 27} 28 29/** 30 * This uses `Portal` magic ✨ to render the icons and title correctly. ItemIcon and ItemText components 31 * get teleported to the top row, leaving the rest of the children in the bottom row. 32 */ 33export function Group({ 34 children, 35 destructive = false, 36 iconInset = true, 37 style, 38 contentContainerStyle, 39}: { 40 children: React.ReactNode 41 destructive?: boolean 42 iconInset?: boolean 43 style?: StyleProp<ViewStyle> 44 contentContainerStyle?: StyleProp<ViewStyle> 45}) { 46 const context = useMemo( 47 () => ({destructive, withinGroup: true}), 48 [destructive], 49 ) 50 return ( 51 <View style={[a.w_full, style]}> 52 <Portal.Provider> 53 <ItemContext.Provider value={context}> 54 <Item style={[a.pb_2xs, {minHeight: 42}]}> 55 <Portal.Outlet /> 56 </Item> 57 <Item 58 style={[ 59 a.flex_col, 60 a.pt_2xs, 61 a.align_start, 62 a.gap_0, 63 contentContainerStyle, 64 ]} 65 iconInset={iconInset}> 66 {children} 67 </Item> 68 </ItemContext.Provider> 69 </Portal.Provider> 70 </View> 71 ) 72} 73 74export function Item({ 75 children, 76 destructive, 77 iconInset = false, 78 style, 79}: { 80 children?: React.ReactNode 81 destructive?: boolean 82 /** 83 * Adds left padding so that the content will be aligned with other Items that contain icons 84 * @default false 85 */ 86 iconInset?: boolean 87 style?: StyleProp<ViewStyle> 88}) { 89 const context = useContext(ItemContext) 90 const childContext = useMemo(() => { 91 if (typeof destructive !== 'boolean') return context 92 return {...context, destructive} 93 }, [context, destructive]) 94 return ( 95 <View 96 style={[ 97 a.px_xl, 98 a.py_sm, 99 a.align_center, 100 a.gap_sm, 101 a.w_full, 102 a.flex_row, 103 {minHeight: 48}, 104 iconInset && { 105 paddingLeft: 106 // existing padding 107 a.pl_xl.paddingLeft + 108 // icon 109 24 + 110 // gap 111 a.gap_sm.gap, 112 }, 113 style, 114 ]}> 115 <ItemContext.Provider value={childContext}> 116 {children} 117 </ItemContext.Provider> 118 </View> 119 ) 120} 121 122export function LinkItem({ 123 children, 124 destructive = false, 125 contentContainerStyle, 126 chevronColor, 127 ...props 128}: Omit<LinkProps, Button.UninheritableButtonProps> & { 129 contentContainerStyle?: StyleProp<ViewStyle> 130 destructive?: boolean 131 chevronColor?: string 132}) { 133 const t = useTheme() 134 135 return ( 136 <Link {...props}> 137 {args => ( 138 <Item 139 destructive={destructive} 140 style={[ 141 (args.hovered || args.pressed) && [t.atoms.bg_contrast_25], 142 contentContainerStyle, 143 ]}> 144 {typeof children === 'function' ? children(args) : children} 145 <Chevron color={chevronColor} /> 146 </Item> 147 )} 148 </Link> 149 ) 150} 151 152export function PressableItem({ 153 children, 154 destructive = false, 155 contentContainerStyle, 156 hoverStyle, 157 ...props 158}: Omit<Button.ButtonProps, Button.UninheritableButtonProps> & { 159 contentContainerStyle?: StyleProp<ViewStyle> 160 destructive?: boolean 161}) { 162 const t = useTheme() 163 return ( 164 <Button.Button {...props}> 165 {args => ( 166 <Item 167 destructive={destructive} 168 style={[ 169 (args.hovered || args.pressed) && [ 170 t.atoms.bg_contrast_25, 171 hoverStyle, 172 ], 173 contentContainerStyle, 174 ]}> 175 {typeof children === 'function' ? children(args) : children} 176 </Item> 177 )} 178 </Button.Button> 179 ) 180} 181 182export function ItemIcon({ 183 icon: Comp, 184 size = 'lg', 185 color: colorProp, 186}: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & { 187 color?: string 188}) { 189 const t = useTheme() 190 const {destructive, withinGroup} = useContext(ItemContext) 191 192 /* 193 * Copied here from icons/common.tsx so we can tweak if we need to, but 194 * also so that we can calculate transforms. 195 */ 196 const iconSize = { 197 '2xs': 8, 198 xs: 12, 199 sm: 16, 200 md: 20, 201 lg: 24, 202 xl: 28, 203 '2xl': 32, 204 '3xl': 40, 205 }[size] 206 207 const color = 208 colorProp ?? (destructive ? t.palette.negative_500 : t.atoms.text.color) 209 210 const content = ( 211 <View style={[a.z_20, {width: iconSize, height: iconSize}]}> 212 <Comp width={iconSize} style={[{color}]} /> 213 </View> 214 ) 215 216 if (withinGroup) { 217 return <Portal.Portal>{content}</Portal.Portal> 218 } else { 219 return content 220 } 221} 222 223export function ItemText({ 224 style, 225 ...props 226}: React.ComponentProps<typeof Button.ButtonText>) { 227 const t = useTheme() 228 const {destructive, withinGroup} = useContext(ItemContext) 229 230 const content = ( 231 <Button.ButtonText 232 style={[ 233 a.text_md, 234 a.font_normal, 235 a.text_left, 236 a.flex_1, 237 destructive ? {color: t.palette.negative_500} : t.atoms.text, 238 style, 239 ]} 240 {...props} 241 /> 242 ) 243 244 if (withinGroup) { 245 return <Portal.Portal>{content}</Portal.Portal> 246 } else { 247 return content 248 } 249} 250 251export function Divider({style}: ViewStyleProp) { 252 const t = useTheme() 253 return ( 254 <View 255 style={[ 256 a.border_t, 257 t.atoms.border_contrast_low, 258 a.w_full, 259 a.my_sm, 260 style, 261 ]} 262 /> 263 ) 264} 265 266export function Chevron({color: colorProp}: {color?: string}) { 267 const {destructive} = useContext(ItemContext) 268 const t = useTheme() 269 const color = 270 colorProp ?? (destructive ? t.palette.negative_500 : t.palette.contrast_500) 271 return <ItemIcon icon={ChevronRightIcon} size="md" color={color} /> 272} 273 274export function BadgeText({ 275 children, 276 style, 277}: { 278 children: React.ReactNode 279 style?: StyleProp<ViewStyle> 280}) { 281 const t = useTheme() 282 return ( 283 <Text 284 style={[ 285 t.atoms.text_contrast_low, 286 a.text_md, 287 a.text_right, 288 a.leading_snug, 289 style, 290 ]} 291 numberOfLines={1}> 292 {children} 293 </Text> 294 ) 295} 296 297export function BadgeButton({ 298 label, 299 onPress, 300}: { 301 label: string 302 onPress: (evt: GestureResponderEvent) => void 303}) { 304 const t = useTheme() 305 return ( 306 <Button.Button label={label} onPress={onPress} hitSlop={HITSLOP_10}> 307 {({pressed}) => ( 308 <Button.ButtonText 309 style={[ 310 a.text_md, 311 a.font_normal, 312 a.text_right, 313 {color: pressed ? t.palette.contrast_300 : t.palette.primary_500}, 314 ]}> 315 {label} 316 </Button.ButtonText> 317 )} 318 </Button.Button> 319 ) 320}