mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at statsig-proxy 569 lines 15 kB view raw
1import React from 'react' 2import { 3 AccessibilityProps, 4 Pressable, 5 PressableProps, 6 StyleProp, 7 StyleSheet, 8 Text, 9 TextProps, 10 TextStyle, 11 View, 12 ViewStyle, 13} from 'react-native' 14import LinearGradient from 'react-native-linear-gradient' 15import {Trans} from '@lingui/macro' 16 17import {android, atoms as a, flatten, tokens, useTheme} from '#/alf' 18import {Props as SVGIconProps} from '#/components/icons/common' 19import {normalizeTextStyles} from '#/components/Typography' 20 21export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' 22export type ButtonColor = 23 | 'primary' 24 | 'secondary' 25 | 'negative' 26 | 'gradient_sky' 27 | 'gradient_midnight' 28 | 'gradient_sunrise' 29 | 'gradient_sunset' 30 | 'gradient_nordic' 31 | 'gradient_bonfire' 32export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large' 33export type ButtonShape = 'round' | 'square' | 'default' 34export type VariantProps = { 35 /** 36 * The style variation of the button 37 */ 38 variant?: ButtonVariant 39 /** 40 * The color of the button 41 */ 42 color?: ButtonColor 43 /** 44 * The size of the button 45 */ 46 size?: ButtonSize 47 /** 48 * The shape of the button 49 */ 50 shape?: ButtonShape 51} 52 53export type ButtonState = { 54 hovered: boolean 55 focused: boolean 56 pressed: boolean 57 disabled: boolean 58} 59 60export type ButtonContext = VariantProps & ButtonState 61 62export type ButtonProps = Pick< 63 PressableProps, 64 'disabled' | 'onPress' | 'testID' 65> & 66 AccessibilityProps & 67 VariantProps & { 68 testID?: string 69 label: string 70 style?: StyleProp<ViewStyle> 71 children: 72 | React.ReactNode 73 | string 74 | ((context: ButtonContext) => React.ReactNode | string) 75 } 76export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} 77 78const Context = React.createContext<VariantProps & ButtonState>({ 79 hovered: false, 80 focused: false, 81 pressed: false, 82 disabled: false, 83}) 84 85export function useButtonContext() { 86 return React.useContext(Context) 87} 88 89export function Button({ 90 children, 91 variant, 92 color, 93 size, 94 shape = 'default', 95 label, 96 disabled = false, 97 style, 98 ...rest 99}: ButtonProps) { 100 const t = useTheme() 101 const [state, setState] = React.useState({ 102 pressed: false, 103 hovered: false, 104 focused: false, 105 }) 106 107 const onPressIn = React.useCallback(() => { 108 setState(s => ({ 109 ...s, 110 pressed: true, 111 })) 112 }, [setState]) 113 const onPressOut = React.useCallback(() => { 114 setState(s => ({ 115 ...s, 116 pressed: false, 117 })) 118 }, [setState]) 119 const onHoverIn = React.useCallback(() => { 120 setState(s => ({ 121 ...s, 122 hovered: true, 123 })) 124 }, [setState]) 125 const onHoverOut = React.useCallback(() => { 126 setState(s => ({ 127 ...s, 128 hovered: false, 129 })) 130 }, [setState]) 131 const onFocus = React.useCallback(() => { 132 setState(s => ({ 133 ...s, 134 focused: true, 135 })) 136 }, [setState]) 137 const onBlur = React.useCallback(() => { 138 setState(s => ({ 139 ...s, 140 focused: false, 141 })) 142 }, [setState]) 143 144 const {baseStyles, hoverStyles} = React.useMemo(() => { 145 const baseStyles: ViewStyle[] = [] 146 const hoverStyles: ViewStyle[] = [] 147 const light = t.name === 'light' 148 149 if (color === 'primary') { 150 if (variant === 'solid') { 151 if (!disabled) { 152 baseStyles.push({ 153 backgroundColor: t.palette.primary_500, 154 }) 155 hoverStyles.push({ 156 backgroundColor: t.palette.primary_600, 157 }) 158 } else { 159 baseStyles.push({ 160 backgroundColor: t.palette.primary_700, 161 }) 162 } 163 } else if (variant === 'outline') { 164 baseStyles.push(a.border, t.atoms.bg, { 165 borderWidth: 1, 166 }) 167 168 if (!disabled) { 169 baseStyles.push(a.border, { 170 borderColor: t.palette.primary_500, 171 }) 172 hoverStyles.push(a.border, { 173 backgroundColor: light 174 ? t.palette.primary_50 175 : t.palette.primary_950, 176 }) 177 } else { 178 baseStyles.push(a.border, { 179 borderColor: light ? t.palette.primary_200 : t.palette.primary_900, 180 }) 181 } 182 } else if (variant === 'ghost') { 183 if (!disabled) { 184 baseStyles.push(t.atoms.bg) 185 hoverStyles.push({ 186 backgroundColor: light 187 ? t.palette.primary_100 188 : t.palette.primary_900, 189 }) 190 } 191 } 192 } else if (color === 'secondary') { 193 if (variant === 'solid') { 194 if (!disabled) { 195 baseStyles.push({ 196 backgroundColor: t.palette.contrast_25, 197 }) 198 hoverStyles.push({ 199 backgroundColor: t.palette.contrast_50, 200 }) 201 } else { 202 baseStyles.push({ 203 backgroundColor: t.palette.contrast_100, 204 }) 205 } 206 } else if (variant === 'outline') { 207 baseStyles.push(a.border, t.atoms.bg, { 208 borderWidth: 1, 209 }) 210 211 if (!disabled) { 212 baseStyles.push(a.border, { 213 borderColor: t.palette.contrast_300, 214 }) 215 hoverStyles.push(t.atoms.bg_contrast_50) 216 } else { 217 baseStyles.push(a.border, { 218 borderColor: t.palette.contrast_200, 219 }) 220 } 221 } else if (variant === 'ghost') { 222 if (!disabled) { 223 baseStyles.push(t.atoms.bg) 224 hoverStyles.push({ 225 backgroundColor: t.palette.contrast_100, 226 }) 227 } 228 } 229 } else if (color === 'negative') { 230 if (variant === 'solid') { 231 if (!disabled) { 232 baseStyles.push({ 233 backgroundColor: t.palette.negative_500, 234 }) 235 hoverStyles.push({ 236 backgroundColor: t.palette.negative_600, 237 }) 238 } else { 239 baseStyles.push({ 240 backgroundColor: t.palette.negative_700, 241 }) 242 } 243 } else if (variant === 'outline') { 244 baseStyles.push(a.border, t.atoms.bg, { 245 borderWidth: 1, 246 }) 247 248 if (!disabled) { 249 baseStyles.push(a.border, { 250 borderColor: t.palette.negative_500, 251 }) 252 hoverStyles.push(a.border, { 253 backgroundColor: light 254 ? t.palette.negative_50 255 : t.palette.negative_975, 256 }) 257 } else { 258 baseStyles.push(a.border, { 259 borderColor: light 260 ? t.palette.negative_200 261 : t.palette.negative_900, 262 }) 263 } 264 } else if (variant === 'ghost') { 265 if (!disabled) { 266 baseStyles.push(t.atoms.bg) 267 hoverStyles.push({ 268 backgroundColor: light 269 ? t.palette.negative_100 270 : t.palette.negative_975, 271 }) 272 } 273 } 274 } 275 276 if (shape === 'default') { 277 if (size === 'large') { 278 baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) 279 } else if (size === 'medium') { 280 baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) 281 } else if (size === 'small') { 282 baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) 283 } else if (size === 'tiny') { 284 baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) 285 } 286 } else if (shape === 'round' || shape === 'square') { 287 if (size === 'large') { 288 if (shape === 'round') { 289 baseStyles.push({height: 54, width: 54}) 290 } else { 291 baseStyles.push({height: 50, width: 50}) 292 } 293 } else if (size === 'small') { 294 baseStyles.push({height: 40, width: 40}) 295 } else if (size === 'tiny') { 296 baseStyles.push({height: 20, width: 20}) 297 } 298 299 if (shape === 'round') { 300 baseStyles.push(a.rounded_full) 301 } else if (shape === 'square') { 302 if (size === 'tiny') { 303 baseStyles.push(a.rounded_xs) 304 } else { 305 baseStyles.push(a.rounded_sm) 306 } 307 } 308 } 309 310 return { 311 baseStyles, 312 hoverStyles, 313 } 314 }, [t, variant, color, size, shape, disabled]) 315 316 const {gradientColors, gradientHoverColors, gradientLocations} = 317 React.useMemo(() => { 318 const colors: string[] = [] 319 const hoverColors: string[] = [] 320 const locations: number[] = [] 321 const gradient = { 322 primary: tokens.gradients.sky, 323 secondary: tokens.gradients.sky, 324 negative: tokens.gradients.sky, 325 gradient_sky: tokens.gradients.sky, 326 gradient_midnight: tokens.gradients.midnight, 327 gradient_sunrise: tokens.gradients.sunrise, 328 gradient_sunset: tokens.gradients.sunset, 329 gradient_nordic: tokens.gradients.nordic, 330 gradient_bonfire: tokens.gradients.bonfire, 331 }[color || 'primary'] 332 333 if (variant === 'gradient') { 334 colors.push(...gradient.values.map(([_, color]) => color)) 335 hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) 336 locations.push(...gradient.values.map(([location, _]) => location)) 337 } 338 339 return { 340 gradientColors: colors, 341 gradientHoverColors: hoverColors, 342 gradientLocations: locations, 343 } 344 }, [variant, color]) 345 346 const context = React.useMemo<ButtonContext>( 347 () => ({ 348 ...state, 349 variant, 350 color, 351 size, 352 disabled: disabled || false, 353 }), 354 [state, variant, color, size, disabled], 355 ) 356 357 const flattenedBaseStyles = flatten(baseStyles) 358 359 return ( 360 <Pressable 361 role="button" 362 accessibilityHint={undefined} // optional 363 {...rest} 364 aria-label={label} 365 aria-pressed={state.pressed} 366 accessibilityLabel={label} 367 disabled={disabled || false} 368 accessibilityState={{ 369 disabled: disabled || false, 370 }} 371 style={[ 372 a.flex_row, 373 a.align_center, 374 a.justify_center, 375 flattenedBaseStyles, 376 ...(state.hovered || state.pressed ? hoverStyles : []), 377 flatten(style), 378 ]} 379 onPressIn={onPressIn} 380 onPressOut={onPressOut} 381 onHoverIn={onHoverIn} 382 onHoverOut={onHoverOut} 383 onFocus={onFocus} 384 onBlur={onBlur}> 385 {variant === 'gradient' && ( 386 <View 387 style={[ 388 a.absolute, 389 a.inset_0, 390 a.overflow_hidden, 391 {borderRadius: flattenedBaseStyles.borderRadius}, 392 ]}> 393 <LinearGradient 394 colors={ 395 state.hovered || state.pressed 396 ? gradientHoverColors 397 : gradientColors 398 } 399 locations={gradientLocations} 400 start={{x: 0, y: 0}} 401 end={{x: 1, y: 1}} 402 style={[a.absolute, a.inset_0]} 403 /> 404 </View> 405 )} 406 <Context.Provider value={context}> 407 {/* @ts-ignore */} 408 {typeof children === 'string' || children?.type === Trans ? ( 409 /* @ts-ignore */ 410 <ButtonText>{children}</ButtonText> 411 ) : typeof children === 'function' ? ( 412 children(context) 413 ) : ( 414 children 415 )} 416 </Context.Provider> 417 </Pressable> 418 ) 419} 420 421export function useSharedButtonTextStyles() { 422 const t = useTheme() 423 const {color, variant, disabled, size} = useButtonContext() 424 return React.useMemo(() => { 425 const baseStyles: TextStyle[] = [] 426 const light = t.name === 'light' 427 428 if (color === 'primary') { 429 if (variant === 'solid') { 430 if (!disabled) { 431 baseStyles.push({color: t.palette.white}) 432 } else { 433 baseStyles.push({color: t.palette.white, opacity: 0.5}) 434 } 435 } else if (variant === 'outline') { 436 if (!disabled) { 437 baseStyles.push({ 438 color: light ? t.palette.primary_600 : t.palette.primary_500, 439 }) 440 } else { 441 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 442 } 443 } else if (variant === 'ghost') { 444 if (!disabled) { 445 baseStyles.push({color: t.palette.primary_600}) 446 } else { 447 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 448 } 449 } 450 } else if (color === 'secondary') { 451 if (variant === 'solid' || variant === 'gradient') { 452 if (!disabled) { 453 baseStyles.push({ 454 color: t.palette.contrast_700, 455 }) 456 } else { 457 baseStyles.push({ 458 color: t.palette.contrast_400, 459 }) 460 } 461 } else if (variant === 'outline') { 462 if (!disabled) { 463 baseStyles.push({ 464 color: t.palette.contrast_600, 465 }) 466 } else { 467 baseStyles.push({ 468 color: t.palette.contrast_300, 469 }) 470 } 471 } else if (variant === 'ghost') { 472 if (!disabled) { 473 baseStyles.push({ 474 color: t.palette.contrast_600, 475 }) 476 } else { 477 baseStyles.push({ 478 color: t.palette.contrast_300, 479 }) 480 } 481 } 482 } else if (color === 'negative') { 483 if (variant === 'solid' || variant === 'gradient') { 484 if (!disabled) { 485 baseStyles.push({color: t.palette.white}) 486 } else { 487 baseStyles.push({color: t.palette.white, opacity: 0.5}) 488 } 489 } else if (variant === 'outline') { 490 if (!disabled) { 491 baseStyles.push({color: t.palette.negative_400}) 492 } else { 493 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 494 } 495 } else if (variant === 'ghost') { 496 if (!disabled) { 497 baseStyles.push({color: t.palette.negative_400}) 498 } else { 499 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 500 } 501 } 502 } else { 503 if (!disabled) { 504 baseStyles.push({color: t.palette.white}) 505 } else { 506 baseStyles.push({color: t.palette.white, opacity: 0.5}) 507 } 508 } 509 510 if (size === 'large') { 511 baseStyles.push(a.text_md, android({paddingBottom: 1})) 512 } else if (size === 'tiny') { 513 baseStyles.push(a.text_xs, android({paddingBottom: 1})) 514 } else { 515 baseStyles.push(a.text_sm, android({paddingBottom: 1})) 516 } 517 518 return StyleSheet.flatten(baseStyles) 519 }, [t, variant, color, size, disabled]) 520} 521 522export function ButtonText({children, style, ...rest}: ButtonTextProps) { 523 const textStyles = useSharedButtonTextStyles() 524 525 return ( 526 <Text 527 {...rest} 528 style={normalizeTextStyles([ 529 a.font_bold, 530 a.text_center, 531 textStyles, 532 style, 533 ])}> 534 {children} 535 </Text> 536 ) 537} 538 539export function ButtonIcon({ 540 icon: Comp, 541 position, 542 size: iconSize, 543}: { 544 icon: React.ComponentType<SVGIconProps> 545 position?: 'left' | 'right' 546 size?: SVGIconProps['size'] 547}) { 548 const {size, disabled} = useButtonContext() 549 const textStyles = useSharedButtonTextStyles() 550 551 return ( 552 <View 553 style={[ 554 a.z_20, 555 { 556 opacity: disabled ? 0.7 : 1, 557 marginLeft: position === 'left' ? -2 : 0, 558 marginRight: position === 'right' ? -2 : 0, 559 }, 560 ]}> 561 <Comp 562 size={ 563 iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm') 564 } 565 style={[{color: textStyles.color, pointerEvents: 'none'}]} 566 /> 567 </View> 568 ) 569}