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