mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 AccessibilityProps,
4 Pressable,
5 PressableProps,
6 StyleProp,
7 StyleSheet,
8 Text,
9 TextProps,
10 TextStyle,
11 View,
12 ViewStyle,
13} from 'react-native'
14import {LinearGradient} from 'expo-linear-gradient'
15
16import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
17import {Props as SVGIconProps} from '#/components/icons/common'
18import {normalizeTextStyles} from '#/components/Typography'
19
20export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
21export type ButtonColor =
22 | 'primary'
23 | 'secondary'
24 | 'secondary_inverted'
25 | 'negative'
26 | 'gradient_sky'
27 | 'gradient_midnight'
28 | 'gradient_sunrise'
29 | 'gradient_sunset'
30 | 'gradient_nordic'
31 | 'gradient_bonfire'
32export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
33export type ButtonShape = 'round' | 'square' | 'default'
34export type VariantProps = {
35 /**
36 * The style variation of the button
37 */
38 variant?: ButtonVariant
39 /**
40 * The color of the button
41 */
42 color?: ButtonColor
43 /**
44 * The size of the button
45 */
46 size?: ButtonSize
47 /**
48 * The shape of the button
49 */
50 shape?: ButtonShape
51}
52
53export type ButtonState = {
54 hovered: boolean
55 focused: boolean
56 pressed: boolean
57 disabled: boolean
58}
59
60export type ButtonContext = VariantProps & ButtonState
61
62type NonTextElements =
63 | React.ReactElement
64 | Iterable<React.ReactElement | null | undefined | boolean>
65
66export type ButtonProps = Pick<
67 PressableProps,
68 'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
69> &
70 AccessibilityProps &
71 VariantProps & {
72 testID?: string
73 label: string
74 style?: StyleProp<ViewStyle>
75 hoverStyle?: StyleProp<ViewStyle>
76 children: NonTextElements | ((context: ButtonContext) => NonTextElements)
77 }
78
79export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
80
81const Context = React.createContext<VariantProps & ButtonState>({
82 hovered: false,
83 focused: false,
84 pressed: false,
85 disabled: false,
86})
87
88export function useButtonContext() {
89 return React.useContext(Context)
90}
91
92export const Button = React.forwardRef<View, ButtonProps>(
93 (
94 {
95 children,
96 variant,
97 color,
98 size,
99 shape = 'default',
100 label,
101 disabled = false,
102 style,
103 hoverStyle: hoverStyleProp,
104 ...rest
105 },
106 ref,
107 ) => {
108 const t = useTheme()
109 const [state, setState] = React.useState({
110 pressed: false,
111 hovered: false,
112 focused: false,
113 })
114
115 const onPressIn = React.useCallback(() => {
116 setState(s => ({
117 ...s,
118 pressed: true,
119 }))
120 }, [setState])
121 const onPressOut = React.useCallback(() => {
122 setState(s => ({
123 ...s,
124 pressed: false,
125 }))
126 }, [setState])
127 const onHoverIn = React.useCallback(() => {
128 setState(s => ({
129 ...s,
130 hovered: true,
131 }))
132 }, [setState])
133 const onHoverOut = React.useCallback(() => {
134 setState(s => ({
135 ...s,
136 hovered: false,
137 }))
138 }, [setState])
139 const onFocus = React.useCallback(() => {
140 setState(s => ({
141 ...s,
142 focused: true,
143 }))
144 }, [setState])
145 const onBlur = React.useCallback(() => {
146 setState(s => ({
147 ...s,
148 focused: false,
149 }))
150 }, [setState])
151
152 const {baseStyles, hoverStyles} = React.useMemo(() => {
153 const baseStyles: ViewStyle[] = []
154 const hoverStyles: ViewStyle[] = []
155 const light = t.name === 'light'
156
157 if (color === 'primary') {
158 if (variant === 'solid') {
159 if (!disabled) {
160 baseStyles.push({
161 backgroundColor: t.palette.primary_500,
162 })
163 hoverStyles.push({
164 backgroundColor: t.palette.primary_600,
165 })
166 } else {
167 baseStyles.push({
168 backgroundColor: t.palette.primary_700,
169 })
170 }
171 } else if (variant === 'outline') {
172 baseStyles.push(a.border, t.atoms.bg, {
173 borderWidth: 1,
174 })
175
176 if (!disabled) {
177 baseStyles.push(a.border, {
178 borderColor: t.palette.primary_500,
179 })
180 hoverStyles.push(a.border, {
181 backgroundColor: light
182 ? t.palette.primary_50
183 : t.palette.primary_950,
184 })
185 } else {
186 baseStyles.push(a.border, {
187 borderColor: light
188 ? t.palette.primary_200
189 : t.palette.primary_900,
190 })
191 }
192 } else if (variant === 'ghost') {
193 if (!disabled) {
194 baseStyles.push(t.atoms.bg)
195 hoverStyles.push({
196 backgroundColor: light
197 ? t.palette.primary_100
198 : t.palette.primary_900,
199 })
200 }
201 }
202 } else if (color === 'secondary') {
203 if (variant === 'solid') {
204 if (!disabled) {
205 baseStyles.push({
206 backgroundColor: t.palette.contrast_25,
207 })
208 hoverStyles.push({
209 backgroundColor: t.palette.contrast_50,
210 })
211 } else {
212 baseStyles.push({
213 backgroundColor: t.palette.contrast_100,
214 })
215 }
216 } else if (variant === 'outline') {
217 baseStyles.push(a.border, t.atoms.bg, {
218 borderWidth: 1,
219 })
220
221 if (!disabled) {
222 baseStyles.push(a.border, {
223 borderColor: t.palette.contrast_300,
224 })
225 hoverStyles.push(t.atoms.bg_contrast_50)
226 } else {
227 baseStyles.push(a.border, {
228 borderColor: t.palette.contrast_200,
229 })
230 }
231 } else if (variant === 'ghost') {
232 if (!disabled) {
233 baseStyles.push(t.atoms.bg)
234 hoverStyles.push({
235 backgroundColor: t.palette.contrast_25,
236 })
237 }
238 }
239 } else if (color === 'secondary_inverted') {
240 if (variant === 'solid') {
241 if (!disabled) {
242 baseStyles.push({
243 backgroundColor: t.palette.contrast_900,
244 })
245 hoverStyles.push({
246 backgroundColor: t.palette.contrast_950,
247 })
248 } else {
249 baseStyles.push({
250 backgroundColor: t.palette.contrast_700,
251 })
252 }
253 } else if (variant === 'outline') {
254 baseStyles.push(a.border, t.atoms.bg, {
255 borderWidth: 1,
256 })
257
258 if (!disabled) {
259 baseStyles.push(a.border, {
260 borderColor: t.palette.contrast_300,
261 })
262 hoverStyles.push(t.atoms.bg_contrast_50)
263 } else {
264 baseStyles.push(a.border, {
265 borderColor: t.palette.contrast_200,
266 })
267 }
268 } else if (variant === 'ghost') {
269 if (!disabled) {
270 baseStyles.push(t.atoms.bg)
271 hoverStyles.push({
272 backgroundColor: t.palette.contrast_25,
273 })
274 }
275 }
276 } else if (color === 'negative') {
277 if (variant === 'solid') {
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 (variant === 'outline') {
291 baseStyles.push(a.border, t.atoms.bg, {
292 borderWidth: 1,
293 })
294
295 if (!disabled) {
296 baseStyles.push(a.border, {
297 borderColor: t.palette.negative_500,
298 })
299 hoverStyles.push(a.border, {
300 backgroundColor: light
301 ? t.palette.negative_50
302 : t.palette.negative_975,
303 })
304 } else {
305 baseStyles.push(a.border, {
306 borderColor: light
307 ? t.palette.negative_200
308 : t.palette.negative_900,
309 })
310 }
311 } else if (variant === 'ghost') {
312 if (!disabled) {
313 baseStyles.push(t.atoms.bg)
314 hoverStyles.push({
315 backgroundColor: light
316 ? t.palette.negative_100
317 : t.palette.negative_975,
318 })
319 }
320 }
321 }
322
323 if (shape === 'default') {
324 if (size === 'large') {
325 baseStyles.push(
326 {paddingVertical: 15},
327 a.px_2xl,
328 a.rounded_sm,
329 a.gap_md,
330 )
331 } else if (size === 'medium') {
332 baseStyles.push(
333 {paddingVertical: 12},
334 a.px_2xl,
335 a.rounded_sm,
336 a.gap_md,
337 )
338 } else if (size === 'small') {
339 baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
340 } else if (size === 'xsmall') {
341 baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
342 } else if (size === 'tiny') {
343 baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
344 }
345 } else if (shape === 'round' || shape === 'square') {
346 if (size === 'large') {
347 if (shape === 'round') {
348 baseStyles.push({height: 54, width: 54})
349 } else {
350 baseStyles.push({height: 50, width: 50})
351 }
352 } else if (size === 'small') {
353 baseStyles.push({height: 34, width: 34})
354 } else if (size === 'xsmall') {
355 baseStyles.push({height: 28, width: 28})
356 } else if (size === 'tiny') {
357 baseStyles.push({height: 20, width: 20})
358 }
359
360 if (shape === 'round') {
361 baseStyles.push(a.rounded_full)
362 } else if (shape === 'square') {
363 if (size === 'tiny') {
364 baseStyles.push(a.rounded_xs)
365 } else {
366 baseStyles.push(a.rounded_sm)
367 }
368 }
369 }
370
371 return {
372 baseStyles,
373 hoverStyles,
374 }
375 }, [t, variant, color, size, shape, disabled])
376
377 const {gradientColors, gradientHoverColors, gradientLocations} =
378 React.useMemo(() => {
379 const colors: string[] = []
380 const hoverColors: string[] = []
381 const locations: number[] = []
382 const gradient = {
383 primary: tokens.gradients.sky,
384 secondary: tokens.gradients.sky,
385 secondary_inverted: tokens.gradients.sky,
386 negative: tokens.gradients.sky,
387 gradient_sky: tokens.gradients.sky,
388 gradient_midnight: tokens.gradients.midnight,
389 gradient_sunrise: tokens.gradients.sunrise,
390 gradient_sunset: tokens.gradients.sunset,
391 gradient_nordic: tokens.gradients.nordic,
392 gradient_bonfire: tokens.gradients.bonfire,
393 }[color || 'primary']
394
395 if (variant === 'gradient') {
396 colors.push(...gradient.values.map(([_, color]) => color))
397 hoverColors.push(...gradient.values.map(_ => gradient.hover_value))
398 locations.push(...gradient.values.map(([location, _]) => location))
399 }
400
401 return {
402 gradientColors: colors,
403 gradientHoverColors: hoverColors,
404 gradientLocations: locations,
405 }
406 }, [variant, color])
407
408 const context = React.useMemo<ButtonContext>(
409 () => ({
410 ...state,
411 variant,
412 color,
413 size,
414 disabled: disabled || false,
415 }),
416 [state, variant, color, size, disabled],
417 )
418
419 const flattenedBaseStyles = flatten(baseStyles)
420
421 return (
422 <Pressable
423 role="button"
424 accessibilityHint={undefined} // optional
425 {...rest}
426 ref={ref}
427 aria-label={label}
428 aria-pressed={state.pressed}
429 accessibilityLabel={label}
430 disabled={disabled || false}
431 accessibilityState={{
432 disabled: disabled || false,
433 }}
434 style={[
435 a.flex_row,
436 a.align_center,
437 a.justify_center,
438 flattenedBaseStyles,
439 flatten(style),
440 ...(state.hovered || state.pressed
441 ? [hoverStyles, flatten(hoverStyleProp)]
442 : []),
443 ]}
444 onPressIn={onPressIn}
445 onPressOut={onPressOut}
446 onHoverIn={onHoverIn}
447 onHoverOut={onHoverOut}
448 onFocus={onFocus}
449 onBlur={onBlur}>
450 {variant === 'gradient' && (
451 <View
452 style={[
453 a.absolute,
454 a.inset_0,
455 a.overflow_hidden,
456 {borderRadius: flattenedBaseStyles.borderRadius},
457 ]}>
458 <LinearGradient
459 colors={
460 state.hovered || state.pressed
461 ? gradientHoverColors
462 : gradientColors
463 }
464 locations={gradientLocations}
465 start={{x: 0, y: 0}}
466 end={{x: 1, y: 1}}
467 style={[a.absolute, a.inset_0]}
468 />
469 </View>
470 )}
471 <Context.Provider value={context}>
472 {typeof children === 'function' ? children(context) : children}
473 </Context.Provider>
474 </Pressable>
475 )
476 },
477)
478Button.displayName = 'Button'
479
480export function useSharedButtonTextStyles() {
481 const t = useTheme()
482 const {color, variant, disabled, size} = useButtonContext()
483 return React.useMemo(() => {
484 const baseStyles: TextStyle[] = []
485 const light = t.name === 'light'
486
487 if (color === 'primary') {
488 if (variant === 'solid') {
489 if (!disabled) {
490 baseStyles.push({color: t.palette.white})
491 } else {
492 baseStyles.push({color: t.palette.white, opacity: 0.5})
493 }
494 } else if (variant === 'outline') {
495 if (!disabled) {
496 baseStyles.push({
497 color: light ? t.palette.primary_600 : t.palette.primary_500,
498 })
499 } else {
500 baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
501 }
502 } else if (variant === 'ghost') {
503 if (!disabled) {
504 baseStyles.push({color: t.palette.primary_600})
505 } else {
506 baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
507 }
508 }
509 } else if (color === 'secondary') {
510 if (variant === 'solid' || variant === 'gradient') {
511 if (!disabled) {
512 baseStyles.push({
513 color: t.palette.contrast_700,
514 })
515 } else {
516 baseStyles.push({
517 color: t.palette.contrast_400,
518 })
519 }
520 } else if (variant === 'outline') {
521 if (!disabled) {
522 baseStyles.push({
523 color: t.palette.contrast_600,
524 })
525 } else {
526 baseStyles.push({
527 color: t.palette.contrast_300,
528 })
529 }
530 } else if (variant === 'ghost') {
531 if (!disabled) {
532 baseStyles.push({
533 color: t.palette.contrast_600,
534 })
535 } else {
536 baseStyles.push({
537 color: t.palette.contrast_300,
538 })
539 }
540 }
541 } else if (color === 'secondary_inverted') {
542 if (variant === 'solid' || variant === 'gradient') {
543 if (!disabled) {
544 baseStyles.push({
545 color: t.palette.contrast_100,
546 })
547 } else {
548 baseStyles.push({
549 color: t.palette.contrast_400,
550 })
551 }
552 } else if (variant === 'outline') {
553 if (!disabled) {
554 baseStyles.push({
555 color: t.palette.contrast_600,
556 })
557 } else {
558 baseStyles.push({
559 color: t.palette.contrast_300,
560 })
561 }
562 } else if (variant === 'ghost') {
563 if (!disabled) {
564 baseStyles.push({
565 color: t.palette.contrast_600,
566 })
567 } else {
568 baseStyles.push({
569 color: t.palette.contrast_300,
570 })
571 }
572 }
573 } else if (color === 'negative') {
574 if (variant === 'solid' || variant === 'gradient') {
575 if (!disabled) {
576 baseStyles.push({color: t.palette.white})
577 } else {
578 baseStyles.push({color: t.palette.white, opacity: 0.5})
579 }
580 } else if (variant === 'outline') {
581 if (!disabled) {
582 baseStyles.push({color: t.palette.negative_400})
583 } else {
584 baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
585 }
586 } else if (variant === 'ghost') {
587 if (!disabled) {
588 baseStyles.push({color: t.palette.negative_400})
589 } else {
590 baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
591 }
592 }
593 } else {
594 if (!disabled) {
595 baseStyles.push({color: t.palette.white})
596 } else {
597 baseStyles.push({color: t.palette.white, opacity: 0.5})
598 }
599 }
600
601 if (size === 'large') {
602 baseStyles.push(a.text_md, android({paddingBottom: 1}))
603 } else if (size === 'tiny') {
604 baseStyles.push(a.text_xs, android({paddingBottom: 1}))
605 } else {
606 baseStyles.push(a.text_sm, android({paddingBottom: 1}))
607 }
608
609 return StyleSheet.flatten(baseStyles)
610 }, [t, variant, color, size, disabled])
611}
612
613export function ButtonText({children, style, ...rest}: ButtonTextProps) {
614 const textStyles = useSharedButtonTextStyles()
615
616 return (
617 <Text
618 {...rest}
619 style={normalizeTextStyles([
620 a.font_bold,
621 a.text_center,
622 textStyles,
623 style,
624 ])}>
625 {children}
626 </Text>
627 )
628}
629
630export function ButtonIcon({
631 icon: Comp,
632 position,
633 size: iconSize,
634}: {
635 icon: React.ComponentType<SVGIconProps>
636 position?: 'left' | 'right'
637 size?: SVGIconProps['size']
638}) {
639 const {size, disabled} = useButtonContext()
640 const textStyles = useSharedButtonTextStyles()
641
642 return (
643 <View
644 style={[
645 a.z_20,
646 {
647 opacity: disabled ? 0.7 : 1,
648 marginLeft: position === 'left' ? -2 : 0,
649 marginRight: position === 'right' ? -2 : 0,
650 },
651 ]}>
652 <Comp
653 size={
654 iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm')
655 }
656 style={[{color: textStyles.color, pointerEvents: 'none'}]}
657 />
658 </View>
659 )
660}