@factorialco/f0-react-native 0.33.0 → 0.35.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 (71) hide show
  1. package/README.md +7 -5
  2. package/lib/module/components/Badge/index.js +1 -1
  3. package/lib/module/components/Badge/index.js.map +1 -1
  4. package/lib/module/components/Button/index.js +1 -1
  5. package/lib/module/components/Button/index.js.map +1 -1
  6. package/lib/module/components/F0Badge/F0Badge.js +2 -0
  7. package/lib/module/components/F0Badge/F0Badge.js.map +1 -0
  8. package/lib/module/components/F0Badge/F0Badge.md +67 -0
  9. package/lib/module/components/F0Badge/F0Badge.styles.js +2 -0
  10. package/lib/module/components/F0Badge/F0Badge.styles.js.map +1 -0
  11. package/lib/module/components/F0Badge/F0Badge.types.js +2 -0
  12. package/lib/module/components/F0Badge/F0Badge.types.js.map +1 -0
  13. package/lib/module/components/F0Badge/index.js +2 -0
  14. package/lib/module/components/F0Badge/index.js.map +1 -0
  15. package/lib/module/components/F0Button/F0Button.js +2 -0
  16. package/lib/module/components/F0Button/F0Button.js.map +1 -0
  17. package/lib/module/components/F0Button/F0Button.md +163 -0
  18. package/lib/module/components/F0Button/F0Button.styles.js +2 -0
  19. package/lib/module/components/F0Button/F0Button.styles.js.map +1 -0
  20. package/lib/module/components/F0Button/F0Button.types.js +2 -0
  21. package/lib/module/components/F0Button/F0Button.types.js.map +1 -0
  22. package/lib/module/components/F0Button/index.js +2 -0
  23. package/lib/module/components/F0Button/index.js.map +1 -0
  24. package/lib/module/components/Icon/index.js.map +1 -1
  25. package/lib/module/components/exports.js +1 -1
  26. package/lib/module/components/exports.js.map +1 -1
  27. package/lib/typescript/components/Badge/index.d.ts +28 -54
  28. package/lib/typescript/components/Badge/index.d.ts.map +1 -1
  29. package/lib/typescript/components/Button/index.d.ts +18 -0
  30. package/lib/typescript/components/Button/index.d.ts.map +1 -1
  31. package/lib/typescript/components/F0Badge/F0Badge.d.ts +5 -0
  32. package/lib/typescript/components/F0Badge/F0Badge.d.ts.map +1 -0
  33. package/lib/typescript/components/F0Badge/F0Badge.styles.d.ts +57 -0
  34. package/lib/typescript/components/F0Badge/F0Badge.styles.d.ts.map +1 -0
  35. package/lib/typescript/components/F0Badge/F0Badge.types.d.ts +19 -0
  36. package/lib/typescript/components/F0Badge/F0Badge.types.d.ts.map +1 -0
  37. package/lib/typescript/components/F0Badge/index.d.ts +4 -0
  38. package/lib/typescript/components/F0Badge/index.d.ts.map +1 -0
  39. package/lib/typescript/components/F0Button/F0Button.d.ts +6 -0
  40. package/lib/typescript/components/F0Button/F0Button.d.ts.map +1 -0
  41. package/lib/typescript/components/F0Button/F0Button.styles.d.ts +161 -0
  42. package/lib/typescript/components/F0Button/F0Button.styles.d.ts.map +1 -0
  43. package/lib/typescript/components/F0Button/F0Button.types.d.ts +47 -0
  44. package/lib/typescript/components/F0Button/F0Button.types.d.ts.map +1 -0
  45. package/lib/typescript/components/F0Button/index.d.ts +4 -0
  46. package/lib/typescript/components/F0Button/index.d.ts.map +1 -0
  47. package/lib/typescript/components/Icon/index.d.ts +5 -0
  48. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  49. package/lib/typescript/components/exports.d.ts +2 -0
  50. package/lib/typescript/components/exports.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/src/components/Badge/__tests__/index.spec.tsx +29 -0
  53. package/src/components/Badge/index.tsx +38 -40
  54. package/src/components/Button/__snapshots__/index.spec.tsx.snap +11 -11
  55. package/src/components/Button/index.tsx +22 -2
  56. package/src/components/F0Badge/F0Badge.md +67 -0
  57. package/src/components/F0Badge/F0Badge.styles.ts +28 -0
  58. package/src/components/F0Badge/F0Badge.tsx +24 -0
  59. package/src/components/F0Badge/F0Badge.types.ts +34 -0
  60. package/src/components/F0Badge/__tests__/F0Badge.spec.tsx +63 -0
  61. package/src/components/F0Badge/__tests__/__snapshots__/F0Badge.spec.tsx.snap +651 -0
  62. package/src/components/F0Badge/index.ts +7 -0
  63. package/src/components/F0Button/F0Button.md +163 -0
  64. package/src/components/F0Button/F0Button.styles.ts +141 -0
  65. package/src/components/F0Button/F0Button.tsx +228 -0
  66. package/src/components/F0Button/F0Button.types.ts +82 -0
  67. package/src/components/F0Button/__tests__/F0Button.spec.tsx +285 -0
  68. package/src/components/F0Button/__tests__/__snapshots__/F0Button.spec.tsx.snap +966 -0
  69. package/src/components/F0Button/index.ts +7 -0
  70. package/src/components/Icon/index.tsx +5 -0
  71. package/src/components/exports.ts +2 -0
@@ -0,0 +1,163 @@
1
+ # F0Button
2
+
3
+ Primary interactive button component for the F0 Design System.
4
+
5
+ ## Overview
6
+
7
+ F0Button is the semantic button API for React Native in F0. It supports:
8
+
9
+ - Visual variants (`default`, `outline`, `critical`, `neutral`, `ghost`, `promote`)
10
+ - Size variants (`sm`, `md`, `lg`)
11
+ - Optional icon / emoji content
12
+ - Icon-only round mode
13
+ - Async press handlers with automatic loading-state handling
14
+ - Visible loading indicator when busy
15
+ - Press feedback control through `PressableFeedback`
16
+
17
+ ## Architecture
18
+
19
+ - **Pattern:** Props API (Atomic) - element order is fixed: icon -> emoji -> label
20
+ - **Press feedback:** Uses `PressableFeedback` component for smooth animations
21
+ - **Press state:** Tracks pressed state with `useState` for color changes by variant
22
+ - **Loading state:** Supports auto-loading when `onPress` returns a Promise
23
+ - **Styling:** `className` and `style` are blocked in the public API and filtered at runtime
24
+ - **Location:** `src/components/F0Button/`
25
+
26
+ ## Usage
27
+
28
+ <!-- prettier-ignore -->
29
+ ```tsx
30
+ import { F0Button } from "@factorialco/f0-react-native"
31
+ import { Archive } from "@factorialco/f0-react-native/icons/app"
32
+
33
+ <F0Button label="Submit" onPress={handleSubmit} />
34
+
35
+ <F0Button label="Delete" variant="critical" icon={Archive} />
36
+
37
+ <F0Button label="Add" icon={Archive} hideLabel round />
38
+
39
+ <F0Button label="Save" onPress={async () => await saveData()} />
40
+
41
+ <F0Button label="Love" emoji="🥰" variant="neutral" />
42
+
43
+ <F0Button label="Custom" feedback="scale" />
44
+ ```
45
+
46
+ ## Props
47
+
48
+ | Prop | Type | Default | Description |
49
+ | ------------------- | -------------------------------- | ----------- | ---------------------------------------------------------- |
50
+ | `label` | `string` | required | Visible label and accessibility base label |
51
+ | `onPress` | `() => void \| Promise<unknown>` | — | Press handler; async return enables auto-loading |
52
+ | `variant` | `F0ButtonVariant` | `"default"` | Visual style variant |
53
+ | `size` | `F0ButtonSize` | `"md"` | Control height and radius |
54
+ | `disabled` | `boolean` | `false` | Disabled state |
55
+ | `loading` | `boolean` | `false` | External loading control |
56
+ | `icon` | `IconType` | — | Optional icon |
57
+ | `emoji` | `string` | — | Optional emoji |
58
+ | `hideLabel` | `boolean` | `false` | Hide visible label (keeps accessibility label) |
59
+ | `round` | `boolean` | `false` | Makes icon-only mode circular |
60
+ | `showBadge` | `boolean` | `false` | Notification badge (outline variant only) |
61
+ | `fullWidth` | `boolean` | `false` | Makes button fill available horizontal space |
62
+ | `feedback` | `PressableFeedbackVariant` | `"both"` | Press feedback mode (`both`, `scale`, `highlight`, `none`) |
63
+ | `accessibilityHint` | `string` | — | Optional screen-reader hint |
64
+ | `testID` | `string` | — | Test identifier |
65
+
66
+ ### Variants
67
+
68
+ - `default` - primary/high-emphasis action
69
+ - `outline` - secondary action with border
70
+ - `critical` - destructive action
71
+ - `neutral` - neutral secondary action
72
+ - `ghost` - subtle transparent action
73
+ - `promote` - promotional/highlighted action
74
+
75
+ ### Sizes
76
+
77
+ - `sm` - compact
78
+ - `md` - default
79
+ - `lg` - large
80
+
81
+ ## Runtime Behavior
82
+
83
+ ### Async onPress auto-loading
84
+
85
+ If `onPress` returns a Promise, F0Button enters internal loading state until it resolves.
86
+
87
+ <!-- prettier-ignore -->
88
+ ```tsx
89
+ <F0Button
90
+ label="Save"
91
+ onPress={async () => {
92
+ await saveData()
93
+ }}
94
+ />
95
+ ```
96
+
97
+ ### Loading visuals
98
+
99
+ When `loading` is `true` (external) or an async `onPress` is pending (internal):
100
+
101
+ - the button becomes non-interactive (`disabled`)
102
+ - a centered spinner indicator is rendered
103
+ - button content is visually hidden (`opacity`) to keep layout stable
104
+
105
+ ### Press feedback
106
+
107
+ F0Button delegates touch feedback to `PressableFeedback` and supports semantic control via `feedback`.
108
+
109
+ <!-- prettier-ignore -->
110
+ ```tsx
111
+ <F0Button label="Both" feedback="both" />
112
+ <F0Button label="Scale only" feedback="scale" />
113
+ <F0Button label="Highlight only" feedback="highlight" />
114
+ <F0Button label="No visual feedback" feedback="none" />
115
+ ```
116
+
117
+ ## Styling Contract
118
+
119
+ F0Button is semantic-first:
120
+
121
+ - `className` is **not** part of `F0ButtonProps`
122
+ - `style` is **not** part of `F0ButtonProps`
123
+ - Both are filtered at runtime before forwarding props to internal pressable primitives
124
+
125
+ This guarantees semantic variants remain source-of-truth and prevents style overrides from bypassing the API contract.
126
+
127
+ ## Accessibility
128
+
129
+ - Always sets `accessibilityRole="button"`
130
+ - Auto-builds `accessibilityLabel` from `label` plus state:
131
+ - `"label, disabled"`
132
+ - `"label, disabled, loading"`
133
+ - Exposes `accessibilityState` with:
134
+ - `disabled`
135
+ - `busy`
136
+ - `hideLabel` does not remove accessibility label
137
+
138
+ ## Testing
139
+
140
+ Main test suite:
141
+
142
+ - `src/components/F0Button/__tests__/F0Button.spec.tsx`
143
+
144
+ Coverage includes:
145
+
146
+ - snapshots across variants/sizes/states
147
+ - async loading behavior
148
+ - accessibility label/state
149
+ - badge rendering rules
150
+ - runtime blocking of `className`/`style`
151
+
152
+ ## File Structure
153
+
154
+ ```
155
+ src/components/F0Button/
156
+ ├── F0Button.tsx
157
+ ├── F0Button.types.ts
158
+ ├── F0Button.styles.ts
159
+ ├── F0Button.md
160
+ ├── __tests__/
161
+ │ └── F0Button.spec.tsx
162
+ └── index.ts
163
+ ```
@@ -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
+ >