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