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