Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
117
fork

Configure Feed

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

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