this repo has no description
0
fork

Configure Feed

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

at e28f6d2f370b4e882ed6f23d08ca0f8d94dbac5f 934 lines 25 kB view raw
1import { 2 createContext, 3 forwardRef, 4 useCallback, 5 useContext, 6 useMemo, 7 useState, 8} from 'react' 9import { 10 type AccessibilityProps, 11 type GestureResponderEvent, 12 type MouseEvent, 13 type NativeSyntheticEvent, 14 Pressable, 15 type PressableProps, 16 type StyleProp, 17 type TargetedEvent, 18 type TextProps, 19 type TextStyle, 20 View, 21 type ViewStyle, 22} from 'react-native' 23 24import {atoms as a, flatten, select, useTheme} from '#/alf' 25import {type Props as SVGIconProps} from '#/components/icons/common' 26import {Text} from '#/components/Typography' 27 28/** 29 * The `Button` component, and some extensions of it like `Link` are intended 30 * to be generic and therefore apply no styles by default. These `VariantProps` 31 * are what control the `Button`'s presentation, and are intended only use cases where the buttons appear as, well, buttons. 32 * 33 * If `Button` or an extension of it are used for other compound components, use this property to avoid misuse of these variant props further down the line. 34 * 35 * @example 36 * type MyComponentProps = Omit<ButtonProps, UninheritableButtonProps> & {...} 37 */ 38export type UninheritableButtonProps = 'variant' | 'color' | 'size' | 'shape' 39 40export type ButtonVariant = 'solid' | 'outline' | 'ghost' 41export type ButtonColor = 42 | 'primary' 43 | 'secondary' 44 | 'secondary_inverted' 45 | 'negative' 46 | 'primary_subtle' 47 | 'negative_subtle' 48export type ButtonSize = 'tiny' | 'small' | 'large' 49export type ButtonShape = 'round' | 'square' | 'rectangular' | 'default' 50export type VariantProps = { 51 /** 52 * The style variation of the button 53 * @deprecated Use `color` instead. 54 */ 55 variant?: ButtonVariant 56 /** 57 * The color of the button 58 */ 59 color?: ButtonColor 60 /** 61 * The size of the button 62 */ 63 size?: ButtonSize 64 /** 65 * The shape of the button 66 * 67 * - `default`: Pill shaped. Most buttons should use this shape. 68 * - `round`: Circular. For icon-only buttons. 69 * - `square`: Square. For icon-only buttons. 70 * - `rectangular`: Rectangular. Matches previous style, use when adjacent to form fields. 71 */ 72 shape?: ButtonShape 73} 74 75export type ButtonState = { 76 hovered: boolean 77 focused: boolean 78 pressed: boolean 79 disabled: boolean 80} 81 82export type ButtonContext = VariantProps & ButtonState 83 84type NonTextElements = 85 | React.ReactElement<any> 86 | Iterable<React.ReactElement<any> | null | undefined | boolean> 87 88export type ButtonProps = Pick< 89 PressableProps, 90 | 'disabled' 91 | 'onPress' 92 | 'testID' 93 | 'onLongPress' 94 | 'hitSlop' 95 | 'onHoverIn' 96 | 'onHoverOut' 97 | 'onPressIn' 98 | 'onPressOut' 99 | 'onFocus' 100 | 'onBlur' 101> & 102 AccessibilityProps & 103 VariantProps & { 104 testID?: string 105 /** 106 * For a11y, try to make this descriptive and clear 107 */ 108 label: string 109 style?: StyleProp<ViewStyle> 110 hoverStyle?: StyleProp<ViewStyle> 111 children: NonTextElements | ((context: ButtonContext) => NonTextElements) 112 PressableComponent?: React.ComponentType<PressableProps> 113 } 114 115export type ButtonTextProps = TextProps & 116 VariantProps & {disabled?: boolean; emoji?: boolean} 117 118const Context = createContext<VariantProps & ButtonState>({ 119 hovered: false, 120 focused: false, 121 pressed: false, 122 disabled: false, 123}) 124Context.displayName = 'ButtonContext' 125 126export function useButtonContext() { 127 return useContext(Context) 128} 129 130export const Button = forwardRef<View, ButtonProps>( 131 ( 132 { 133 children, 134 variant, 135 color, 136 size, 137 shape = 'default', 138 label, 139 disabled = false, 140 style, 141 hoverStyle: hoverStyleProp, 142 PressableComponent = Pressable, 143 onPressIn: onPressInOuter, 144 onPressOut: onPressOutOuter, 145 onHoverIn: onHoverInOuter, 146 onHoverOut: onHoverOutOuter, 147 onFocus: onFocusOuter, 148 onBlur: onBlurOuter, 149 ...rest 150 }, 151 ref, 152 ) => { 153 /** 154 * The `variant` prop is deprecated in favor of simply specifying `color`. 155 * If a `color` is set, then we want to use the existing codepaths for 156 * "solid" buttons. This is to maintain backwards compatibility. 157 */ 158 if (!variant && color) { 159 variant = 'solid' 160 } 161 162 const t = useTheme() 163 const [state, setState] = useState({ 164 pressed: false, 165 hovered: false, 166 focused: false, 167 }) 168 169 const onPressIn = useCallback( 170 (e: GestureResponderEvent) => { 171 setState(s => ({ 172 ...s, 173 pressed: true, 174 })) 175 onPressInOuter?.(e) 176 }, 177 [setState, onPressInOuter], 178 ) 179 const onPressOut = useCallback( 180 (e: GestureResponderEvent) => { 181 setState(s => ({ 182 ...s, 183 pressed: false, 184 })) 185 onPressOutOuter?.(e) 186 }, 187 [setState, onPressOutOuter], 188 ) 189 const onHoverIn = useCallback( 190 (e: MouseEvent) => { 191 setState(s => ({ 192 ...s, 193 hovered: true, 194 })) 195 onHoverInOuter?.(e) 196 }, 197 [setState, onHoverInOuter], 198 ) 199 const onHoverOut = useCallback( 200 (e: MouseEvent) => { 201 setState(s => ({ 202 ...s, 203 hovered: false, 204 })) 205 onHoverOutOuter?.(e) 206 }, 207 [setState, onHoverOutOuter], 208 ) 209 const onFocus = useCallback( 210 (e: NativeSyntheticEvent<TargetedEvent>) => { 211 setState(s => ({ 212 ...s, 213 focused: true, 214 })) 215 onFocusOuter?.(e) 216 }, 217 [setState, onFocusOuter], 218 ) 219 const onBlur = useCallback( 220 (e: NativeSyntheticEvent<TargetedEvent>) => { 221 setState(s => ({ 222 ...s, 223 focused: false, 224 })) 225 onBlurOuter?.(e) 226 }, 227 [setState, onBlurOuter], 228 ) 229 230 const {baseStyles, hoverStyles} = useMemo(() => { 231 const baseStyles: ViewStyle[] = [] 232 const hoverStyles: ViewStyle[] = [] 233 234 /* 235 * This is the happy path for new button styles, following the 236 * deprecation of `variant` prop. This redundant `variant` check is here 237 * just to make this handling easier to understand. 238 */ 239 if (variant === 'solid') { 240 if (color === 'primary') { 241 if (!disabled) { 242 baseStyles.push({ 243 backgroundColor: t.palette.primary_500, 244 }) 245 hoverStyles.push({ 246 backgroundColor: t.palette.primary_600, 247 }) 248 } else { 249 baseStyles.push({ 250 backgroundColor: t.palette.primary_200, 251 }) 252 } 253 } else if (color === 'secondary') { 254 if (!disabled) { 255 baseStyles.push(t.atoms.bg_contrast_50) 256 hoverStyles.push(t.atoms.bg_contrast_100) 257 } else { 258 baseStyles.push(t.atoms.bg_contrast_50) 259 } 260 } else if (color === 'secondary_inverted') { 261 if (!disabled) { 262 baseStyles.push({ 263 backgroundColor: t.palette.contrast_900, 264 }) 265 hoverStyles.push({ 266 backgroundColor: t.palette.contrast_975, 267 }) 268 } else { 269 baseStyles.push({ 270 backgroundColor: t.palette.contrast_600, 271 }) 272 } 273 } else if (color === 'negative') { 274 if (!disabled) { 275 baseStyles.push({ 276 backgroundColor: t.palette.negative_500, 277 }) 278 hoverStyles.push({ 279 backgroundColor: t.palette.negative_600, 280 }) 281 } else { 282 baseStyles.push({ 283 backgroundColor: t.palette.negative_700, 284 }) 285 } 286 } else if (color === 'primary_subtle') { 287 if (!disabled) { 288 baseStyles.push({ 289 backgroundColor: t.palette.primary_50, 290 }) 291 hoverStyles.push({ 292 backgroundColor: t.palette.primary_100, 293 }) 294 } else { 295 baseStyles.push({ 296 backgroundColor: t.palette.primary_50, 297 }) 298 } 299 } else if (color === 'negative_subtle') { 300 if (!disabled) { 301 baseStyles.push({ 302 backgroundColor: t.palette.negative_50, 303 }) 304 hoverStyles.push({ 305 backgroundColor: t.palette.negative_100, 306 }) 307 } else { 308 baseStyles.push({ 309 backgroundColor: t.palette.negative_50, 310 }) 311 } 312 } 313 } else { 314 /* 315 * BEGIN DEPRECATED STYLES 316 */ 317 if (color === 'primary') { 318 if (variant === 'outline') { 319 baseStyles.push(a.border, t.atoms.bg, { 320 borderWidth: 1, 321 }) 322 323 if (!disabled) { 324 baseStyles.push(a.border, { 325 borderColor: t.palette.primary_500, 326 }) 327 hoverStyles.push(a.border, { 328 backgroundColor: t.palette.primary_50, 329 }) 330 } else { 331 baseStyles.push(a.border, { 332 borderColor: t.palette.primary_200, 333 }) 334 } 335 } else if (variant === 'ghost') { 336 if (!disabled) { 337 baseStyles.push(t.atoms.bg) 338 hoverStyles.push({ 339 backgroundColor: t.palette.primary_100, 340 }) 341 } 342 } 343 } else if (color === 'secondary') { 344 if (variant === 'outline') { 345 baseStyles.push(a.border, t.atoms.bg, { 346 borderWidth: 1, 347 }) 348 349 if (!disabled) { 350 baseStyles.push(a.border, { 351 borderColor: t.palette.contrast_300, 352 }) 353 hoverStyles.push(t.atoms.bg_contrast_50) 354 } else { 355 baseStyles.push(a.border, { 356 borderColor: t.palette.contrast_200, 357 }) 358 } 359 } else if (variant === 'ghost') { 360 if (!disabled) { 361 baseStyles.push(t.atoms.bg) 362 hoverStyles.push({ 363 backgroundColor: t.palette.contrast_50, 364 }) 365 } 366 } 367 } else if (color === 'secondary_inverted') { 368 if (variant === 'outline') { 369 baseStyles.push(a.border, t.atoms.bg, { 370 borderWidth: 1, 371 }) 372 373 if (!disabled) { 374 baseStyles.push(a.border, { 375 borderColor: t.palette.contrast_300, 376 }) 377 hoverStyles.push(t.atoms.bg_contrast_50) 378 } else { 379 baseStyles.push(a.border, { 380 borderColor: t.palette.contrast_200, 381 }) 382 } 383 } else if (variant === 'ghost') { 384 if (!disabled) { 385 baseStyles.push(t.atoms.bg) 386 hoverStyles.push({ 387 backgroundColor: t.palette.contrast_50, 388 }) 389 } 390 } 391 } else if (color === 'negative') { 392 if (variant === 'outline') { 393 baseStyles.push(a.border, t.atoms.bg, { 394 borderWidth: 1, 395 }) 396 397 if (!disabled) { 398 baseStyles.push(a.border, { 399 borderColor: t.palette.negative_500, 400 }) 401 hoverStyles.push(a.border, { 402 backgroundColor: t.palette.negative_50, 403 }) 404 } else { 405 baseStyles.push(a.border, { 406 borderColor: t.palette.negative_200, 407 }) 408 } 409 } else if (variant === 'ghost') { 410 if (!disabled) { 411 baseStyles.push(t.atoms.bg) 412 hoverStyles.push({ 413 backgroundColor: t.palette.negative_100, 414 }) 415 } 416 } 417 } else if (color === 'negative_subtle') { 418 if (variant === 'outline') { 419 baseStyles.push(a.border, t.atoms.bg, { 420 borderWidth: 1, 421 }) 422 423 if (!disabled) { 424 baseStyles.push(a.border, { 425 borderColor: t.palette.negative_500, 426 }) 427 hoverStyles.push(a.border, { 428 backgroundColor: t.palette.negative_50, 429 }) 430 } else { 431 baseStyles.push(a.border, { 432 borderColor: t.palette.negative_200, 433 }) 434 } 435 } else if (variant === 'ghost') { 436 if (!disabled) { 437 baseStyles.push(t.atoms.bg) 438 hoverStyles.push({ 439 backgroundColor: t.palette.negative_100, 440 }) 441 } 442 } 443 } 444 /* 445 * END DEPRECATED STYLES 446 */ 447 } 448 449 if (shape === 'default') { 450 if (size === 'large') { 451 baseStyles.push(a.rounded_full, { 452 paddingVertical: 12, 453 paddingHorizontal: 24, 454 gap: 6, 455 }) 456 } else if (size === 'small') { 457 baseStyles.push(a.rounded_full, { 458 paddingVertical: 8, 459 paddingHorizontal: 14, 460 gap: 5, 461 }) 462 } else if (size === 'tiny') { 463 baseStyles.push(a.rounded_full, { 464 paddingVertical: 5, 465 paddingHorizontal: 10, 466 gap: 3, 467 }) 468 } 469 } else if (shape === 'rectangular') { 470 if (size === 'large') { 471 baseStyles.push({ 472 paddingVertical: 12, 473 paddingHorizontal: 25, 474 borderRadius: 10, 475 gap: 3, 476 }) 477 } else if (size === 'small') { 478 baseStyles.push({ 479 paddingVertical: 8, 480 paddingHorizontal: 13, 481 borderRadius: 8, 482 gap: 3, 483 }) 484 } else if (size === 'tiny') { 485 baseStyles.push({ 486 paddingVertical: 5, 487 paddingHorizontal: 9, 488 borderRadius: 6, 489 gap: 2, 490 }) 491 } 492 } else if (shape === 'round' || shape === 'square') { 493 /* 494 * These sizes match the actual rendered size on screen, based on 495 * Chrome's web inspector 496 */ 497 if (size === 'large') { 498 if (shape === 'round') { 499 baseStyles.push({height: 44, width: 44}) 500 } else { 501 baseStyles.push({height: 44, width: 44}) 502 } 503 } else if (size === 'small') { 504 if (shape === 'round') { 505 baseStyles.push({height: 33, width: 33}) 506 } else { 507 baseStyles.push({height: 33, width: 33}) 508 } 509 } else if (size === 'tiny') { 510 if (shape === 'round') { 511 baseStyles.push({height: 25, width: 25}) 512 } else { 513 baseStyles.push({height: 25, width: 25}) 514 } 515 } 516 517 if (shape === 'round') { 518 baseStyles.push(a.rounded_full) 519 } else if (shape === 'square') { 520 if (size === 'tiny') { 521 baseStyles.push({ 522 borderRadius: 6, 523 }) 524 } else { 525 baseStyles.push(a.rounded_sm) 526 } 527 } 528 } 529 530 return { 531 baseStyles, 532 hoverStyles, 533 } 534 }, [t, variant, color, size, shape, disabled]) 535 536 const context = useMemo<ButtonContext>( 537 () => ({ 538 ...state, 539 variant, 540 color, 541 size, 542 shape, 543 disabled: disabled || false, 544 }), 545 [state, variant, color, size, shape, disabled], 546 ) 547 548 return ( 549 <PressableComponent 550 role="button" 551 accessibilityHint={undefined} // optional 552 {...rest} 553 // @ts-ignore - this will always be a pressable 554 ref={ref} 555 aria-label={label} 556 aria-pressed={state.pressed} 557 accessibilityLabel={label} 558 disabled={disabled || false} 559 accessibilityState={{ 560 disabled: disabled || false, 561 }} 562 style={[ 563 a.flex_row, 564 a.align_center, 565 a.justify_center, 566 a.curve_continuous, 567 baseStyles, 568 style, 569 ...(state.hovered || state.pressed 570 ? [hoverStyles, hoverStyleProp] 571 : []), 572 ]} 573 onPressIn={onPressIn} 574 onPressOut={onPressOut} 575 onHoverIn={onHoverIn} 576 onHoverOut={onHoverOut} 577 onFocus={onFocus} 578 onBlur={onBlur}> 579 <Context.Provider value={context}> 580 {typeof children === 'function' ? children(context) : children} 581 </Context.Provider> 582 </PressableComponent> 583 ) 584 }, 585) 586Button.displayName = 'Button' 587 588export function useSharedButtonTextStyles() { 589 const t = useTheme() 590 const {color, variant, disabled, size} = useButtonContext() 591 return useMemo(() => { 592 const baseStyles: TextStyle[] = [] 593 594 /* 595 * This is the happy path for new button styles, following the 596 * deprecation of `variant` prop. This redundant `variant` check is here 597 * just to make this handling easier to understand. 598 */ 599 if (variant === 'solid') { 600 if (color === 'primary') { 601 if (!disabled) { 602 baseStyles.push({color: t.palette.white}) 603 } else { 604 baseStyles.push({ 605 color: select(t.name, { 606 light: t.palette.white, 607 dim: t.atoms.text_inverted.color, 608 dark: t.atoms.text_inverted.color, 609 }), 610 }) 611 } 612 } else if (color === 'secondary') { 613 if (!disabled) { 614 baseStyles.push(t.atoms.text_contrast_medium) 615 } else { 616 baseStyles.push({ 617 color: t.palette.contrast_300, 618 }) 619 } 620 } else if (color === 'secondary_inverted') { 621 if (!disabled) { 622 baseStyles.push(t.atoms.text_inverted) 623 } else { 624 baseStyles.push({ 625 color: t.palette.contrast_300, 626 }) 627 } 628 } else if (color === 'negative') { 629 if (!disabled) { 630 baseStyles.push({color: t.palette.white}) 631 } else { 632 baseStyles.push({color: t.palette.negative_300}) 633 } 634 } else if (color === 'primary_subtle') { 635 if (!disabled) { 636 baseStyles.push({ 637 color: t.palette.primary_600, 638 }) 639 } else { 640 baseStyles.push({ 641 color: t.palette.primary_200, 642 }) 643 } 644 } else if (color === 'negative_subtle') { 645 if (!disabled) { 646 baseStyles.push({ 647 color: t.palette.negative_600, 648 }) 649 } else { 650 baseStyles.push({ 651 color: t.palette.negative_200, 652 }) 653 } 654 } 655 } else { 656 /* 657 * BEGIN DEPRECATED STYLES 658 */ 659 if (color === 'primary') { 660 if (variant === 'outline') { 661 if (!disabled) { 662 baseStyles.push({ 663 color: t.palette.primary_600, 664 }) 665 } else { 666 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 667 } 668 } else if (variant === 'ghost') { 669 if (!disabled) { 670 baseStyles.push({color: t.palette.primary_600}) 671 } else { 672 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 673 } 674 } 675 } else if (color === 'secondary') { 676 if (variant === 'outline') { 677 if (!disabled) { 678 baseStyles.push({ 679 color: t.palette.contrast_600, 680 }) 681 } else { 682 baseStyles.push({ 683 color: t.palette.contrast_300, 684 }) 685 } 686 } else if (variant === 'ghost') { 687 if (!disabled) { 688 baseStyles.push({ 689 color: t.palette.contrast_600, 690 }) 691 } else { 692 baseStyles.push({ 693 color: t.palette.contrast_300, 694 }) 695 } 696 } 697 } else if (color === 'secondary_inverted') { 698 if (variant === 'outline') { 699 if (!disabled) { 700 baseStyles.push({ 701 color: t.palette.contrast_600, 702 }) 703 } else { 704 baseStyles.push({ 705 color: t.palette.contrast_300, 706 }) 707 } 708 } else if (variant === 'ghost') { 709 if (!disabled) { 710 baseStyles.push({ 711 color: t.palette.contrast_600, 712 }) 713 } else { 714 baseStyles.push({ 715 color: t.palette.contrast_300, 716 }) 717 } 718 } 719 } else if (color === 'negative') { 720 if (variant === 'outline') { 721 if (!disabled) { 722 baseStyles.push({color: t.palette.negative_400}) 723 } else { 724 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 725 } 726 } else if (variant === 'ghost') { 727 if (!disabled) { 728 baseStyles.push({color: t.palette.negative_400}) 729 } else { 730 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 731 } 732 } 733 } else if (color === 'negative_subtle') { 734 if (variant === 'outline') { 735 if (!disabled) { 736 baseStyles.push({color: t.palette.negative_400}) 737 } else { 738 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 739 } 740 } else if (variant === 'ghost') { 741 if (!disabled) { 742 baseStyles.push({color: t.palette.negative_400}) 743 } else { 744 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 745 } 746 } 747 } 748 /* 749 * END DEPRECATED STYLES 750 */ 751 } 752 753 if (size === 'large') { 754 baseStyles.push(a.text_md, a.leading_snug, a.font_medium) 755 } else if (size === 'small') { 756 baseStyles.push(a.text_sm, a.leading_snug, a.font_medium) 757 } else if (size === 'tiny') { 758 baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold) 759 } 760 761 return flatten(baseStyles) 762 }, [t, variant, color, size, disabled]) 763} 764 765export function ButtonText({children, style, ...rest}: ButtonTextProps) { 766 const textStyles = useSharedButtonTextStyles() 767 768 return ( 769 <Text {...rest} style={[a.text_center, textStyles, style]}> 770 {children} 771 </Text> 772 ) 773} 774 775export function ButtonIcon({ 776 icon: Comp, 777 size, 778}: { 779 icon: React.ComponentType<SVGIconProps> 780 /** 781 * @deprecated no longer needed 782 */ 783 position?: 'left' | 'right' 784 size?: SVGIconProps['size'] 785}) { 786 const {size: buttonSize, shape: buttonShape} = useButtonContext() 787 const textStyles = useSharedButtonTextStyles() 788 const {iconSize, iconContainerSize, iconNegativeMargin} = useMemo(() => { 789 /** 790 * Pre-set icon sizes for different button sizes 791 */ 792 const iconSizeShorthand = 793 size ?? 794 (({ 795 large: 'md', 796 small: 'sm', 797 tiny: 'xs', 798 }[buttonSize || 'small'] || 'sm') as Exclude< 799 SVGIconProps['size'], 800 undefined 801 >) 802 803 /* 804 * Copied here from icons/common.tsx so we can tweak if we need to, but 805 * also so that we can calculate transforms. 806 */ 807 const iconSize = { 808 xs: 12, 809 sm: 16, 810 md: 18, 811 lg: 24, 812 xl: 28, 813 '2xs': 8, 814 '2xl': 32, 815 '3xl': 40, 816 }[iconSizeShorthand] 817 818 /* 819 * Goal here is to match rendered text size so that different size icons 820 * don't increase button size 821 */ 822 const iconContainerSize = { 823 large: 20, 824 small: 17, 825 tiny: 15, 826 }[buttonSize || 'small'] 827 828 /* 829 * The icon needs to be closer to the edge of the button than the text. Therefore 830 * we make the gap slightly too large, and then pull in the sides using negative margins. 831 */ 832 let iconNegativeMargin = 0 833 834 if (buttonShape === 'default') { 835 iconNegativeMargin = { 836 large: -2, 837 small: -2, 838 tiny: -1, 839 }[buttonSize || 'small'] 840 } 841 842 return { 843 iconSize, 844 iconContainerSize, 845 iconNegativeMargin, 846 } 847 }, [buttonSize, buttonShape, size]) 848 849 return ( 850 <View 851 style={[ 852 a.z_20, 853 { 854 width: size === '2xs' ? 10 : iconContainerSize, 855 height: iconContainerSize, 856 marginLeft: iconNegativeMargin, 857 marginRight: iconNegativeMargin, 858 }, 859 ]}> 860 <View 861 style={[ 862 a.absolute, 863 { 864 width: iconSize, 865 height: iconSize, 866 top: '50%', 867 left: '50%', 868 transform: [ 869 { 870 translateX: (iconSize / 2) * -1, 871 }, 872 { 873 translateY: (iconSize / 2) * -1, 874 }, 875 ], 876 }, 877 ]}> 878 <Comp 879 width={iconSize} 880 style={[ 881 { 882 color: textStyles.color, 883 pointerEvents: 'none', 884 }, 885 ]} 886 /> 887 </View> 888 </View> 889 ) 890} 891 892export type StackedButtonProps = Omit< 893 ButtonProps, 894 keyof VariantProps | 'children' 895> & 896 Pick<VariantProps, 'color'> & { 897 children: React.ReactNode 898 icon: React.ComponentType<SVGIconProps> 899 } 900 901export function StackedButton({children, ...props}: StackedButtonProps) { 902 return ( 903 <Button 904 {...props} 905 size="tiny" 906 style={[ 907 a.flex_col, 908 { 909 height: 72, 910 paddingHorizontal: 16, 911 borderRadius: 20, 912 gap: 4, 913 }, 914 props.style, 915 ]}> 916 <StackedButtonInnerText icon={props.icon}> 917 {children} 918 </StackedButtonInnerText> 919 </Button> 920 ) 921} 922 923function StackedButtonInnerText({ 924 children, 925 icon: Icon, 926}: Pick<StackedButtonProps, 'icon' | 'children'>) { 927 const textStyles = useSharedButtonTextStyles() 928 return ( 929 <> 930 <Icon width={24} fill={textStyles.color} /> 931 <ButtonText>{children}</ButtonText> 932 </> 933 ) 934}