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