Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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