@factorialco/f0-react-native 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +7 -5
  2. package/lib/module/components/Button/index.js +1 -1
  3. package/lib/module/components/Button/index.js.map +1 -1
  4. package/lib/module/components/F0Button/F0Button.js +2 -0
  5. package/lib/module/components/F0Button/F0Button.js.map +1 -0
  6. package/lib/module/components/F0Button/F0Button.md +163 -0
  7. package/lib/module/components/F0Button/F0Button.styles.js +2 -0
  8. package/lib/module/components/F0Button/F0Button.styles.js.map +1 -0
  9. package/lib/module/components/F0Button/F0Button.types.js +2 -0
  10. package/lib/module/components/F0Button/F0Button.types.js.map +1 -0
  11. package/lib/module/components/F0Button/index.js +2 -0
  12. package/lib/module/components/F0Button/index.js.map +1 -0
  13. package/lib/module/components/Icon/index.js.map +1 -1
  14. package/lib/module/components/exports.js +1 -1
  15. package/lib/module/components/exports.js.map +1 -1
  16. package/lib/module/components/primitives/F0Image/F0Image.js +2 -0
  17. package/lib/module/components/primitives/F0Image/F0Image.js.map +1 -0
  18. package/lib/module/components/primitives/F0Image/F0Image.md +40 -0
  19. package/lib/module/components/primitives/F0Image/F0Image.types.js +2 -0
  20. package/lib/module/components/primitives/F0Image/F0Image.types.js.map +1 -0
  21. package/lib/module/components/primitives/F0Image/index.js +2 -0
  22. package/lib/module/components/primitives/F0Image/index.js.map +1 -0
  23. package/lib/typescript/components/Button/index.d.ts +18 -0
  24. package/lib/typescript/components/Button/index.d.ts.map +1 -1
  25. package/lib/typescript/components/F0Button/F0Button.d.ts +6 -0
  26. package/lib/typescript/components/F0Button/F0Button.d.ts.map +1 -0
  27. package/lib/typescript/components/F0Button/F0Button.styles.d.ts +161 -0
  28. package/lib/typescript/components/F0Button/F0Button.styles.d.ts.map +1 -0
  29. package/lib/typescript/components/F0Button/F0Button.types.d.ts +47 -0
  30. package/lib/typescript/components/F0Button/F0Button.types.d.ts.map +1 -0
  31. package/lib/typescript/components/F0Button/index.d.ts +4 -0
  32. package/lib/typescript/components/F0Button/index.d.ts.map +1 -0
  33. package/lib/typescript/components/Icon/index.d.ts +5 -0
  34. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  35. package/lib/typescript/components/exports.d.ts +2 -0
  36. package/lib/typescript/components/exports.d.ts.map +1 -1
  37. package/lib/typescript/components/primitives/F0Image/F0Image.d.ts +11 -0
  38. package/lib/typescript/components/primitives/F0Image/F0Image.d.ts.map +1 -0
  39. package/lib/typescript/components/primitives/F0Image/F0Image.types.d.ts +21 -0
  40. package/lib/typescript/components/primitives/F0Image/F0Image.types.d.ts.map +1 -0
  41. package/lib/typescript/components/primitives/F0Image/index.d.ts +3 -0
  42. package/lib/typescript/components/primitives/F0Image/index.d.ts.map +1 -0
  43. package/package.json +2 -1
  44. package/src/components/Button/__snapshots__/index.spec.tsx.snap +11 -11
  45. package/src/components/Button/index.tsx +22 -2
  46. package/src/components/F0Button/F0Button.md +163 -0
  47. package/src/components/F0Button/F0Button.styles.ts +141 -0
  48. package/src/components/F0Button/F0Button.tsx +228 -0
  49. package/src/components/F0Button/F0Button.types.ts +82 -0
  50. package/src/components/F0Button/__tests__/F0Button.spec.tsx +285 -0
  51. package/src/components/F0Button/__tests__/__snapshots__/F0Button.spec.tsx.snap +966 -0
  52. package/src/components/F0Button/index.ts +7 -0
  53. package/src/components/Icon/index.tsx +5 -0
  54. package/src/components/exports.ts +2 -0
  55. package/src/components/primitives/F0Image/F0Image.md +40 -0
  56. package/src/components/primitives/F0Image/F0Image.tsx +48 -0
  57. package/src/components/primitives/F0Image/F0Image.types.ts +23 -0
  58. package/src/components/primitives/F0Image/__tests__/F0Image.spec.tsx +46 -0
  59. package/src/components/primitives/F0Image/index.ts +2 -0
@@ -0,0 +1,141 @@
1
+ import { tv } from "tailwind-variants"
2
+
3
+ import type { IconColor } from "../primitives/F0Icon"
4
+ import type { TextColor } from "../primitives/F0Text"
5
+
6
+ import type { ButtonVariant } from "./F0Button.types"
7
+
8
+ export const buttonVariants = tv({
9
+ base: "flex-row items-center justify-center rounded border-none grow-0",
10
+ variants: {
11
+ variant: {
12
+ default: "bg-f0-background-accent-bold",
13
+ outline: "bg-f0-background-inverse-secondary border border-f0-border",
14
+ neutral: "bg-f0-background-secondary",
15
+ critical: "bg-f0-background-secondary border border-f0-border",
16
+ ghost: "bg-transparent",
17
+ promote: "bg-f0-background-promote border border-f0-border-promote",
18
+ },
19
+ size: {
20
+ sm: "h-6 rounded-sm",
21
+ md: "h-8 rounded",
22
+ lg: "h-10 rounded-md",
23
+ },
24
+ disabled: {
25
+ true: "opacity-50",
26
+ false: "",
27
+ },
28
+ round: {
29
+ true: "aspect-square p-0",
30
+ false: "gap-1 px-2 sm:px-3 lg:px-4",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "default",
35
+ size: "md",
36
+ disabled: false,
37
+ round: false,
38
+ },
39
+ })
40
+
41
+ export const pressedVariants = tv({
42
+ base: "",
43
+ variants: {
44
+ variant: {
45
+ default: "bg-f0-background-accent-bold-hover",
46
+ outline: "bg-f0-background-tertiary border-opacity-70",
47
+ neutral: "bg-f0-background-secondary-hover",
48
+ critical: "bg-f0-background-critical-bold border-transparent",
49
+ ghost: "bg-f0-background-secondary-hover",
50
+ promote: "bg-f0-background-promote-hover",
51
+ },
52
+ },
53
+ defaultVariants: {
54
+ variant: "default",
55
+ },
56
+ })
57
+
58
+ export const loadingContentVariants = tv({
59
+ variants: {
60
+ loading: {
61
+ true: "opacity-0",
62
+ false: "opacity-100",
63
+ },
64
+ },
65
+ defaultVariants: {
66
+ loading: false,
67
+ },
68
+ })
69
+
70
+ export const loadingIndicatorVariants = tv({
71
+ base: "rounded-full border-solid border-t-transparent",
72
+ variants: {
73
+ variant: {
74
+ default: "border-f0-foreground-inverse",
75
+ outline: "border-f0-foreground",
76
+ neutral: "border-f0-foreground",
77
+ critical: "border-f0-icon-critical",
78
+ ghost: "border-f0-foreground",
79
+ promote: "border-f0-icon-promote",
80
+ },
81
+ size: {
82
+ sm: "h-3 w-3 border",
83
+ md: "h-4 w-4 border-2",
84
+ lg: "h-5 w-5 border-2",
85
+ },
86
+ },
87
+ defaultVariants: {
88
+ variant: "default",
89
+ size: "md",
90
+ },
91
+ })
92
+
93
+ export const getIconColor = (
94
+ variant: ButtonVariant,
95
+ isPressed: boolean
96
+ ): IconColor => {
97
+ switch (variant) {
98
+ case "default":
99
+ return "inverse"
100
+ case "critical":
101
+ return isPressed ? "inverse" : "critical-bold"
102
+ default:
103
+ return "default"
104
+ }
105
+ }
106
+
107
+ export const getIconOnlyColor = (
108
+ variant: ButtonVariant,
109
+ isPressed: boolean
110
+ ): IconColor => {
111
+ switch (variant) {
112
+ case "critical":
113
+ return isPressed ? "inverse" : "critical-bold"
114
+ case "default":
115
+ return "inverse"
116
+ case "outline":
117
+ case "neutral":
118
+ case "ghost":
119
+ case "promote":
120
+ default:
121
+ return "bold"
122
+ }
123
+ }
124
+
125
+ export const getTextColor = (
126
+ variant: ButtonVariant,
127
+ isPressed: boolean
128
+ ): TextColor => {
129
+ if (isPressed && variant === "critical") {
130
+ return "inverse"
131
+ }
132
+
133
+ switch (variant) {
134
+ case "default":
135
+ return "inverse"
136
+ case "critical":
137
+ return "critical"
138
+ default:
139
+ return "default"
140
+ }
141
+ }
@@ -0,0 +1,228 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from "react"
8
+ import { View } from "react-native"
9
+ import Animated, {
10
+ Easing,
11
+ cancelAnimation,
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withRepeat,
15
+ withTiming,
16
+ } from "react-native-reanimated"
17
+
18
+ import { cn, omitProps } from "../../lib/utils"
19
+ import { F0Icon } from "../primitives/F0Icon"
20
+ import { F0Text } from "../primitives/F0Text"
21
+ import { PressableFeedback } from "../primitives/PressableFeedback"
22
+
23
+ import {
24
+ buttonVariants,
25
+ loadingContentVariants,
26
+ loadingIndicatorVariants,
27
+ pressedVariants,
28
+ getIconColor,
29
+ getIconOnlyColor,
30
+ getTextColor,
31
+ } from "./F0Button.styles"
32
+ import {
33
+ F0_BUTTON_BLOCKED_FORWARD_PROPS,
34
+ type F0ButtonProps,
35
+ } from "./F0Button.types"
36
+
37
+ const F0Button = React.memo(
38
+ forwardRef<View, F0ButtonProps>(function F0Button(
39
+ {
40
+ label,
41
+ onPress,
42
+ disabled = false,
43
+ loading = false,
44
+ icon,
45
+ emoji,
46
+ hideLabel = false,
47
+ variant = "default",
48
+ size = "md",
49
+ round = false,
50
+ accessibilityHint,
51
+ showBadge = false,
52
+ fullWidth = false,
53
+ testID,
54
+ feedback = "both",
55
+ ...rest
56
+ },
57
+ ref
58
+ ) {
59
+ const [isLoading, setIsLoading] = useState(false)
60
+ const [isPressed, setIsPressed] = useState(false)
61
+ const spinnerRotation = useSharedValue(0)
62
+
63
+ const isBusy = loading || isLoading
64
+ const isDisabled = disabled || isBusy
65
+ const isRound = hideLabel && round
66
+
67
+ const handlePress = useCallback(async () => {
68
+ if (!onPress || isDisabled) return
69
+
70
+ try {
71
+ const result = onPress()
72
+
73
+ if (result && typeof result.then === "function") {
74
+ setIsLoading(true)
75
+ try {
76
+ await result
77
+ } catch {
78
+ // Avoid bubbling async handler rejections from a design-system component.
79
+ } finally {
80
+ setIsLoading(false)
81
+ }
82
+ }
83
+ } catch {
84
+ // Avoid bubbling sync exceptions from a design-system component.
85
+ }
86
+ }, [onPress, isDisabled])
87
+
88
+ useEffect(() => {
89
+ if (!isBusy) {
90
+ cancelAnimation(spinnerRotation)
91
+ spinnerRotation.value = 0
92
+ return
93
+ }
94
+
95
+ spinnerRotation.value = 0
96
+ spinnerRotation.value = withRepeat(
97
+ withTiming(1, {
98
+ duration: 1000,
99
+ easing: Easing.linear,
100
+ }),
101
+ -1,
102
+ false
103
+ )
104
+ }, [isBusy, spinnerRotation])
105
+
106
+ const handlePressIn = useCallback(() => {
107
+ if (!isDisabled) {
108
+ setIsPressed(true)
109
+ }
110
+ }, [isDisabled])
111
+
112
+ const handlePressOut = useCallback(() => {
113
+ setIsPressed(false)
114
+ }, [])
115
+
116
+ const baseClassName = useMemo(
117
+ () =>
118
+ buttonVariants({
119
+ variant,
120
+ size,
121
+ disabled: isDisabled,
122
+ round: isRound,
123
+ }),
124
+ [variant, size, isDisabled, isRound]
125
+ )
126
+
127
+ const accessibilityLabel = useMemo(() => {
128
+ const parts = [label]
129
+ if (isDisabled) parts.push("disabled")
130
+ if (isBusy) parts.push("loading")
131
+ return parts.join(", ")
132
+ }, [label, isBusy, isDisabled])
133
+
134
+ const shouldShowPressed = isPressed && !isDisabled
135
+
136
+ const className = shouldShowPressed
137
+ ? cn(baseClassName, pressedVariants({ variant }))
138
+ : baseClassName
139
+
140
+ const iconIsOnly = isRound || (hideLabel && !emoji)
141
+ const iconColor = icon
142
+ ? iconIsOnly
143
+ ? getIconOnlyColor(variant, shouldShowPressed)
144
+ : getIconColor(variant, shouldShowPressed)
145
+ : undefined
146
+ const textColor = getTextColor(variant, shouldShowPressed)
147
+ const forwardedProps = omitProps(rest, F0_BUTTON_BLOCKED_FORWARD_PROPS)
148
+ const loadingIndicatorStyle = useAnimatedStyle(() => {
149
+ return {
150
+ borderTopColor: "transparent",
151
+ transform: [{ rotateZ: `${spinnerRotation.value * 360}deg` }],
152
+ }
153
+ })
154
+
155
+ return (
156
+ <View className={`flex ${fullWidth ? "flex-1" : "items-start"}`}>
157
+ <PressableFeedback
158
+ ref={ref}
159
+ {...forwardedProps}
160
+ disabled={isDisabled}
161
+ variant={feedback}
162
+ onPress={handlePress}
163
+ onPressIn={handlePressIn}
164
+ onPressOut={handlePressOut}
165
+ accessibilityLabel={accessibilityLabel}
166
+ accessibilityRole="button"
167
+ accessibilityState={{
168
+ disabled: isDisabled,
169
+ busy: isBusy,
170
+ }}
171
+ accessibilityHint={accessibilityHint}
172
+ testID={testID}
173
+ >
174
+ <View className={cn("relative", className)}>
175
+ <View
176
+ testID="f0-button-content"
177
+ className={loadingContentVariants({ loading: isBusy })}
178
+ >
179
+ <View className="flex-row items-center justify-center gap-1">
180
+ {icon && (
181
+ <F0Icon
182
+ icon={icon}
183
+ size="lg"
184
+ className={isRound ? undefined : "-ml-0.5"}
185
+ color={iconColor}
186
+ />
187
+ )}
188
+ {emoji && (
189
+ <F0Text variant="body-md-medium" color={textColor}>
190
+ {emoji}
191
+ </F0Text>
192
+ )}
193
+ {!hideLabel && (
194
+ <F0Text variant="body-md-medium" color={textColor}>
195
+ {label}
196
+ </F0Text>
197
+ )}
198
+ </View>
199
+ </View>
200
+ {isBusy && (
201
+ <View
202
+ pointerEvents="none"
203
+ className="absolute inset-0 items-center justify-center"
204
+ >
205
+ <Animated.View
206
+ testID="f0-button-loading-indicator"
207
+ accessibilityLabel="Loading indicator"
208
+ className={loadingIndicatorVariants({ variant, size })}
209
+ style={loadingIndicatorStyle}
210
+ />
211
+ </View>
212
+ )}
213
+ </View>
214
+ </PressableFeedback>
215
+ {showBadge && variant === "outline" && (
216
+ <View
217
+ accessibilityLabel="Notification Badge"
218
+ className="absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full bg-f0-icon-accent"
219
+ />
220
+ )}
221
+ </View>
222
+ )
223
+ })
224
+ )
225
+
226
+ F0Button.displayName = "F0Button"
227
+
228
+ export { F0Button }
@@ -0,0 +1,82 @@
1
+ import type { IconType } from "../primitives/F0Icon"
2
+ import type {
3
+ PressableFeedbackProps,
4
+ PressableFeedbackVariant,
5
+ } from "../primitives/PressableFeedback"
6
+
7
+ /**
8
+ * Button variant types
9
+ */
10
+ export const BUTTON_VARIANTS = [
11
+ "default",
12
+ "outline",
13
+ "critical",
14
+ "neutral",
15
+ "ghost",
16
+ "promote",
17
+ ] as const
18
+
19
+ export type ButtonVariant = (typeof BUTTON_VARIANTS)[number]
20
+ export type F0ButtonVariant = ButtonVariant
21
+
22
+ /**
23
+ * Button size types
24
+ */
25
+ export const BUTTON_SIZES = ["sm", "md", "lg"] as const
26
+
27
+ export type ButtonSize = (typeof BUTTON_SIZES)[number]
28
+ export type F0ButtonSize = ButtonSize
29
+
30
+ /**
31
+ * Props that are controlled by F0Button and must not be passed through.
32
+ * This preserves F0Button press-state behavior and accessibility contract.
33
+ */
34
+ const F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS = [
35
+ "onPressIn",
36
+ "onPressOut",
37
+ "accessibilityLabel",
38
+ "accessibilityRole",
39
+ "accessibilityState",
40
+ ] as const
41
+
42
+ export const F0_BUTTON_BANNED_PROPS = ["style", "className"] as const
43
+
44
+ export const F0_BUTTON_BLOCKED_FORWARD_PROPS = [
45
+ ...F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS,
46
+ ...F0_BUTTON_BANNED_PROPS,
47
+ ] as const
48
+
49
+ interface F0ButtonPropsInternal extends Omit<
50
+ PressableFeedbackProps,
51
+ | "children"
52
+ | "variant"
53
+ | "disabled"
54
+ | (typeof F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS)[number]
55
+ > {
56
+ label: string
57
+ onPress?: () => void | Promise<unknown>
58
+ variant?: ButtonVariant
59
+ size?: ButtonSize
60
+ disabled?: boolean
61
+ loading?: boolean
62
+ icon?: IconType
63
+ emoji?: string
64
+ hideLabel?: boolean
65
+ round?: boolean
66
+ showBadge?: boolean
67
+ fullWidth?: boolean
68
+ accessibilityHint?: string
69
+ testID?: string
70
+ feedback?: PressableFeedbackVariant
71
+ }
72
+
73
+ /**
74
+ * Public props for the F0Button component.
75
+ *
76
+ * Note: `className` and `style` props are NOT available.
77
+ * Use semantic props (variant, size, etc.) for styling.
78
+ */
79
+ export type F0ButtonProps = Omit<
80
+ F0ButtonPropsInternal,
81
+ (typeof F0_BUTTON_BANNED_PROPS)[number]
82
+ >