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