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