forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}