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

Configure Feed

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

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