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