@onlynative/components 0.1.0-alpha.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 (87) hide show
  1. package/README.md +99 -0
  2. package/dist/appbar/index.d.ts +71 -0
  3. package/dist/appbar/index.js +952 -0
  4. package/dist/button/index.d.ts +41 -0
  5. package/dist/button/index.js +454 -0
  6. package/dist/card/index.d.ts +31 -0
  7. package/dist/card/index.js +264 -0
  8. package/dist/checkbox/index.d.ts +25 -0
  9. package/dist/checkbox/index.js +291 -0
  10. package/dist/chip/index.d.ts +62 -0
  11. package/dist/chip/index.js +452 -0
  12. package/dist/icon-button/index.d.ts +10 -0
  13. package/dist/icon-button/index.js +575 -0
  14. package/dist/index.d.ts +19 -0
  15. package/dist/index.js +3374 -0
  16. package/dist/layout/index.d.ts +98 -0
  17. package/dist/layout/index.js +282 -0
  18. package/dist/list/index.d.ts +60 -0
  19. package/dist/list/index.js +300 -0
  20. package/dist/radio/index.d.ts +25 -0
  21. package/dist/radio/index.js +250 -0
  22. package/dist/switch/index.d.ts +37 -0
  23. package/dist/switch/index.js +315 -0
  24. package/dist/text-field/index.d.ts +52 -0
  25. package/dist/text-field/index.js +496 -0
  26. package/dist/types-D3hlyvz-.d.ts +51 -0
  27. package/dist/typography/index.d.ts +28 -0
  28. package/dist/typography/index.js +69 -0
  29. package/package.json +166 -0
  30. package/src/appbar/AppBar.tsx +302 -0
  31. package/src/appbar/index.ts +2 -0
  32. package/src/appbar/styles.ts +92 -0
  33. package/src/appbar/types.ts +67 -0
  34. package/src/button/Button.tsx +130 -0
  35. package/src/button/index.ts +2 -0
  36. package/src/button/styles.ts +288 -0
  37. package/src/button/types.ts +42 -0
  38. package/src/card/Card.tsx +69 -0
  39. package/src/card/index.ts +2 -0
  40. package/src/card/styles.ts +151 -0
  41. package/src/card/types.ts +27 -0
  42. package/src/checkbox/Checkbox.tsx +109 -0
  43. package/src/checkbox/index.ts +2 -0
  44. package/src/checkbox/styles.ts +155 -0
  45. package/src/checkbox/types.ts +20 -0
  46. package/src/chip/Chip.tsx +182 -0
  47. package/src/chip/index.ts +2 -0
  48. package/src/chip/styles.ts +240 -0
  49. package/src/chip/types.ts +58 -0
  50. package/src/icon-button/IconButton.tsx +358 -0
  51. package/src/icon-button/index.ts +6 -0
  52. package/src/icon-button/styles.ts +259 -0
  53. package/src/icon-button/types.ts +55 -0
  54. package/src/index.ts +51 -0
  55. package/src/layout/Box.tsx +99 -0
  56. package/src/layout/Column.tsx +16 -0
  57. package/src/layout/Grid.tsx +49 -0
  58. package/src/layout/Layout.tsx +81 -0
  59. package/src/layout/Row.tsx +22 -0
  60. package/src/layout/index.ts +13 -0
  61. package/src/layout/resolveSpacing.ts +11 -0
  62. package/src/layout/types.ts +82 -0
  63. package/src/list/List.tsx +17 -0
  64. package/src/list/ListDivider.tsx +20 -0
  65. package/src/list/ListItem.tsx +128 -0
  66. package/src/list/index.ts +9 -0
  67. package/src/list/styles.ts +132 -0
  68. package/src/list/types.ts +54 -0
  69. package/src/radio/Radio.tsx +103 -0
  70. package/src/radio/index.ts +2 -0
  71. package/src/radio/styles.ts +139 -0
  72. package/src/radio/types.ts +20 -0
  73. package/src/switch/Switch.tsx +118 -0
  74. package/src/switch/index.ts +2 -0
  75. package/src/switch/styles.ts +172 -0
  76. package/src/switch/types.ts +32 -0
  77. package/src/test-utils/render-with-theme.tsx +13 -0
  78. package/src/text-field/TextField.tsx +298 -0
  79. package/src/text-field/index.ts +2 -0
  80. package/src/text-field/styles.ts +240 -0
  81. package/src/text-field/types.ts +49 -0
  82. package/src/typography/Typography.tsx +65 -0
  83. package/src/typography/index.ts +3 -0
  84. package/src/typography/types.ts +17 -0
  85. package/src/utils/color.ts +64 -0
  86. package/src/utils/elevation.ts +33 -0
  87. package/src/utils/rtl.ts +19 -0
@@ -0,0 +1,20 @@
1
+ import type { PressableProps } from 'react-native'
2
+
3
+ export interface RadioProps extends Omit<PressableProps, 'children'> {
4
+ /**
5
+ * Whether the radio button is selected.
6
+ * @default false
7
+ */
8
+ value?: boolean
9
+ /** Callback fired when the radio is pressed. Receives the new value. */
10
+ onValueChange?: (value: boolean) => void
11
+ /**
12
+ * Override the outer ring and inner dot color when selected.
13
+ * State-layer colors (hover, press) are derived automatically.
14
+ */
15
+ containerColor?: string
16
+ /**
17
+ * Override the outer ring color when unselected.
18
+ */
19
+ contentColor?: string
20
+ }
@@ -0,0 +1,118 @@
1
+ import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
2
+ import { useMemo } from 'react'
3
+ import { Platform, Pressable, StyleSheet, View } from 'react-native'
4
+ import type { StyleProp, ViewStyle } from 'react-native'
5
+ import { useTheme } from '@onlynative/core'
6
+
7
+ import { createStyles } from './styles'
8
+ import type { SwitchProps } from './types'
9
+
10
+ interface PressableState {
11
+ pressed: boolean
12
+ hovered?: boolean
13
+ }
14
+
15
+ function resolveStyle(
16
+ trackStyle: StyleProp<ViewStyle>,
17
+ hoveredTrackStyle: StyleProp<ViewStyle>,
18
+ pressedTrackStyle: StyleProp<ViewStyle>,
19
+ disabledTrackStyle: StyleProp<ViewStyle>,
20
+ disabled: boolean,
21
+ style: SwitchProps['style'],
22
+ ): (state: PressableState) => StyleProp<ViewStyle> {
23
+ if (typeof style === 'function') {
24
+ return (state) => [
25
+ trackStyle,
26
+ state.hovered && !state.pressed && !disabled
27
+ ? hoveredTrackStyle
28
+ : undefined,
29
+ state.pressed && !disabled ? pressedTrackStyle : undefined,
30
+ disabled ? disabledTrackStyle : undefined,
31
+ style(state),
32
+ ]
33
+ }
34
+
35
+ return (state) => [
36
+ trackStyle,
37
+ state.hovered && !state.pressed && !disabled
38
+ ? hoveredTrackStyle
39
+ : undefined,
40
+ state.pressed && !disabled ? pressedTrackStyle : undefined,
41
+ disabled ? disabledTrackStyle : undefined,
42
+ style,
43
+ ]
44
+ }
45
+
46
+ export function Switch({
47
+ style,
48
+ value = false,
49
+ onValueChange,
50
+ selectedIcon = 'check',
51
+ unselectedIcon,
52
+ containerColor,
53
+ contentColor,
54
+ disabled = false,
55
+ ...props
56
+ }: SwitchProps) {
57
+ const isDisabled = Boolean(disabled)
58
+ const isSelected = Boolean(value)
59
+ const hasIcon = isSelected || Boolean(unselectedIcon)
60
+
61
+ const theme = useTheme()
62
+ const styles = useMemo(
63
+ () =>
64
+ createStyles(theme, isSelected, hasIcon, containerColor, contentColor),
65
+ [theme, isSelected, hasIcon, containerColor, contentColor],
66
+ )
67
+
68
+ const resolvedIconColor = useMemo(() => {
69
+ const base = StyleSheet.flatten([
70
+ styles.iconColor,
71
+ isDisabled ? styles.disabledIconColor : undefined,
72
+ ])
73
+ return typeof base?.color === 'string' ? base.color : undefined
74
+ }, [styles.iconColor, styles.disabledIconColor, isDisabled])
75
+
76
+ const handlePress = () => {
77
+ if (!isDisabled) {
78
+ onValueChange?.(!isSelected)
79
+ }
80
+ }
81
+
82
+ const iconName = isSelected ? selectedIcon : unselectedIcon
83
+ const iconSize = 16
84
+
85
+ return (
86
+ <Pressable
87
+ {...props}
88
+ accessibilityRole="switch"
89
+ accessibilityState={{
90
+ disabled: isDisabled,
91
+ checked: isSelected,
92
+ }}
93
+ hitSlop={Platform.OS === 'web' ? undefined : 4}
94
+ disabled={isDisabled}
95
+ onPress={handlePress}
96
+ style={resolveStyle(
97
+ styles.track,
98
+ styles.hoveredTrack,
99
+ styles.pressedTrack,
100
+ styles.disabledTrack,
101
+ isDisabled,
102
+ style,
103
+ )}
104
+ >
105
+ <View
106
+ style={[styles.thumb, isDisabled ? styles.disabledThumb : undefined]}
107
+ >
108
+ {iconName ? (
109
+ <MaterialCommunityIcons
110
+ name={iconName}
111
+ size={iconSize}
112
+ color={resolvedIconColor}
113
+ />
114
+ ) : null}
115
+ </View>
116
+ </Pressable>
117
+ )
118
+ }
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch'
2
+ export type { SwitchProps } from './types'
@@ -0,0 +1,172 @@
1
+ import { StyleSheet } from 'react-native'
2
+ import type { Theme } from '@onlynative/core'
3
+
4
+ import { alphaColor, blendColor } from '../utils/color'
5
+
6
+ interface TrackColors {
7
+ trackColor: string
8
+ thumbColor: string
9
+ iconColor: string
10
+ hoveredTrackColor: string
11
+ pressedTrackColor: string
12
+ borderColor: string
13
+ borderWidth: number
14
+ disabledTrackColor: string
15
+ disabledThumbColor: string
16
+ disabledBorderColor: string
17
+ disabledBorderWidth: number
18
+ }
19
+
20
+ function getColors(theme: Theme, selected: boolean): TrackColors {
21
+ const disabledOnSurface12 = alphaColor(theme.colors.onSurface, 0.12)
22
+ const disabledOnSurface38 = alphaColor(theme.colors.onSurface, 0.38)
23
+
24
+ if (selected) {
25
+ return {
26
+ trackColor: theme.colors.primary,
27
+ thumbColor: theme.colors.onPrimary,
28
+ iconColor: theme.colors.onPrimaryContainer,
29
+ hoveredTrackColor: blendColor(
30
+ theme.colors.primary,
31
+ theme.colors.onPrimary,
32
+ theme.stateLayer.hoveredOpacity,
33
+ ),
34
+ pressedTrackColor: blendColor(
35
+ theme.colors.primary,
36
+ theme.colors.onPrimary,
37
+ theme.stateLayer.pressedOpacity,
38
+ ),
39
+ borderColor: 'transparent',
40
+ borderWidth: 0,
41
+ disabledTrackColor: disabledOnSurface12,
42
+ disabledThumbColor: theme.colors.surface,
43
+ disabledBorderColor: 'transparent',
44
+ disabledBorderWidth: 0,
45
+ }
46
+ }
47
+
48
+ return {
49
+ trackColor: theme.colors.surfaceContainerHighest,
50
+ thumbColor: theme.colors.outline,
51
+ iconColor: theme.colors.surfaceContainerHighest,
52
+ hoveredTrackColor: blendColor(
53
+ theme.colors.surfaceContainerHighest,
54
+ theme.colors.onSurface,
55
+ theme.stateLayer.hoveredOpacity,
56
+ ),
57
+ pressedTrackColor: blendColor(
58
+ theme.colors.surfaceContainerHighest,
59
+ theme.colors.onSurface,
60
+ theme.stateLayer.pressedOpacity,
61
+ ),
62
+ borderColor: theme.colors.outline,
63
+ borderWidth: 2,
64
+ disabledTrackColor: disabledOnSurface12,
65
+ disabledThumbColor: disabledOnSurface38,
66
+ disabledBorderColor: disabledOnSurface12,
67
+ disabledBorderWidth: 2,
68
+ }
69
+ }
70
+
71
+ function applyColorOverrides(
72
+ theme: Theme,
73
+ colors: TrackColors,
74
+ containerColor?: string,
75
+ contentColor?: string,
76
+ ): TrackColors {
77
+ if (!containerColor && !contentColor) return colors
78
+
79
+ const result = { ...colors }
80
+
81
+ if (contentColor) {
82
+ result.thumbColor = contentColor
83
+ result.iconColor = contentColor
84
+ }
85
+
86
+ if (containerColor) {
87
+ const overlay = contentColor ?? colors.thumbColor
88
+ result.trackColor = containerColor
89
+ result.borderColor = containerColor
90
+ result.hoveredTrackColor = blendColor(
91
+ containerColor,
92
+ overlay,
93
+ theme.stateLayer.hoveredOpacity,
94
+ )
95
+ result.pressedTrackColor = blendColor(
96
+ containerColor,
97
+ overlay,
98
+ theme.stateLayer.pressedOpacity,
99
+ )
100
+ if (contentColor) {
101
+ result.iconColor = containerColor
102
+ }
103
+ }
104
+
105
+ return result
106
+ }
107
+
108
+ export function createStyles(
109
+ theme: Theme,
110
+ selected: boolean,
111
+ hasIcon: boolean,
112
+ containerColor?: string,
113
+ contentColor?: string,
114
+ ) {
115
+ const colors = applyColorOverrides(
116
+ theme,
117
+ getColors(theme, selected),
118
+ containerColor,
119
+ contentColor,
120
+ )
121
+
122
+ const thumbSize = selected || hasIcon ? 24 : 16
123
+ const trackWidth = 52
124
+ const trackHeight = 32
125
+ const trackPadding = 4
126
+ const thumbOffset = selected
127
+ ? trackWidth - trackPadding - thumbSize
128
+ : trackPadding
129
+
130
+ return StyleSheet.create({
131
+ track: {
132
+ width: trackWidth,
133
+ height: trackHeight,
134
+ borderRadius: trackHeight / 2,
135
+ backgroundColor: colors.trackColor,
136
+ borderColor: colors.borderColor,
137
+ borderWidth: colors.borderWidth,
138
+ justifyContent: 'center',
139
+ cursor: 'pointer',
140
+ },
141
+ hoveredTrack: {
142
+ backgroundColor: colors.hoveredTrackColor,
143
+ },
144
+ pressedTrack: {
145
+ backgroundColor: colors.pressedTrackColor,
146
+ },
147
+ disabledTrack: {
148
+ backgroundColor: colors.disabledTrackColor,
149
+ borderColor: colors.disabledBorderColor,
150
+ borderWidth: colors.disabledBorderWidth,
151
+ cursor: 'auto',
152
+ },
153
+ thumb: {
154
+ width: thumbSize,
155
+ height: thumbSize,
156
+ borderRadius: thumbSize / 2,
157
+ backgroundColor: colors.thumbColor,
158
+ marginStart: thumbOffset,
159
+ alignItems: 'center' as const,
160
+ justifyContent: 'center' as const,
161
+ },
162
+ disabledThumb: {
163
+ backgroundColor: colors.disabledThumbColor,
164
+ },
165
+ iconColor: {
166
+ color: colors.iconColor,
167
+ },
168
+ disabledIconColor: {
169
+ color: alphaColor(theme.colors.onSurface, 0.38),
170
+ },
171
+ })
172
+ }
@@ -0,0 +1,32 @@
1
+ import type MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
2
+ import type { ComponentProps } from 'react'
3
+ import type { PressableProps } from 'react-native'
4
+
5
+ export interface SwitchProps extends Omit<PressableProps, 'children'> {
6
+ /**
7
+ * Whether the switch is toggled on.
8
+ * @default false
9
+ */
10
+ value?: boolean
11
+ /** Callback fired when the switch is toggled. Receives the new value. */
12
+ onValueChange?: (value: boolean) => void
13
+ /**
14
+ * Name of a MaterialCommunityIcons icon to show on the thumb when selected.
15
+ * @default 'check'
16
+ */
17
+ selectedIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
18
+ /**
19
+ * Name of a MaterialCommunityIcons icon to show on the thumb when unselected.
20
+ * When provided, the thumb renders at the larger selected size.
21
+ */
22
+ unselectedIcon?: ComponentProps<typeof MaterialCommunityIcons>['name']
23
+ /**
24
+ * Override the track color.
25
+ * State-layer colors (hover, press) are derived automatically.
26
+ */
27
+ containerColor?: string
28
+ /**
29
+ * Override the thumb and icon color.
30
+ */
31
+ contentColor?: string
32
+ }
@@ -0,0 +1,13 @@
1
+ import { render, type RenderOptions } from '@testing-library/react-native'
2
+ import type { ReactElement } from 'react'
3
+ import { MaterialProvider } from '@onlynative/core'
4
+
5
+ export function renderWithTheme(
6
+ ui: ReactElement,
7
+ options?: Omit<RenderOptions, 'wrapper'>,
8
+ ) {
9
+ return render(ui, {
10
+ wrapper: ({ children }) => <MaterialProvider>{children}</MaterialProvider>,
11
+ ...options,
12
+ })
13
+ }
@@ -0,0 +1,298 @@
1
+ import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
+ import {
4
+ Animated,
5
+ Platform,
6
+ Pressable,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native'
11
+ import type { NativeSyntheticEvent, TargetedEvent } from 'react-native'
12
+ import { useTheme } from '@onlynative/core'
13
+
14
+ import { createStyles, labelPositions } from './styles'
15
+ import type { TextFieldProps } from './types'
16
+
17
+ const ICON_SIZE = 24
18
+ // 12dp icon inset + 24dp icon + 16dp gap
19
+ const ICON_WITH_GAP = 12 + 24 + 16
20
+
21
+ export function TextField({
22
+ value,
23
+ onChangeText,
24
+ label,
25
+ placeholder,
26
+ variant = 'filled',
27
+ supportingText,
28
+ errorText,
29
+ error = false,
30
+ disabled = false,
31
+ leadingIcon,
32
+ trailingIcon,
33
+ onTrailingIconPress,
34
+ multiline = false,
35
+ onFocus,
36
+ onBlur,
37
+ style,
38
+ containerColor,
39
+ contentColor,
40
+ inputStyle,
41
+ ...textInputProps
42
+ }: TextFieldProps) {
43
+ const theme = useTheme()
44
+ const isDisabled = Boolean(disabled)
45
+ const isError = Boolean(error) || Boolean(errorText)
46
+ const isFilled = variant === 'filled'
47
+ const hasLeadingIcon = Boolean(leadingIcon)
48
+
49
+ const { colors, styles } = useMemo(
50
+ () => createStyles(theme, variant),
51
+ [theme, variant],
52
+ )
53
+
54
+ const [isFocused, setIsFocused] = useState(false)
55
+ const [internalHasText, setInternalHasText] = useState(
56
+ () => value !== undefined && value !== '',
57
+ )
58
+ const inputRef = useRef<TextInput>(null)
59
+
60
+ const isControlled = value !== undefined
61
+ const hasValue = isControlled ? value !== '' : internalHasText
62
+ const isLabelFloated = isFocused || hasValue
63
+
64
+ // Animation: 0 = resting (label large, centered), 1 = floated (label small, top)
65
+ const labelAnimRef = useRef(new Animated.Value(isLabelFloated ? 1 : 0))
66
+ const labelAnim = labelAnimRef.current
67
+
68
+ useEffect(() => {
69
+ Animated.timing(labelAnim, {
70
+ toValue: isLabelFloated ? 1 : 0,
71
+ duration: 150,
72
+ useNativeDriver: Platform.OS !== 'web',
73
+ }).start()
74
+ }, [isLabelFloated, labelAnim])
75
+
76
+ // Scale: bodyLarge/bodySmall when resting → 1.0 when floated.
77
+ // Label is always rendered at bodySmall size; scale makes it appear as bodyLarge.
78
+ const labelScale = useMemo(() => {
79
+ const restingScale =
80
+ theme.typography.bodyLarge.fontSize / theme.typography.bodySmall.fontSize
81
+ return labelAnim.interpolate({
82
+ inputRange: [0, 1],
83
+ outputRange: [restingScale, 1],
84
+ })
85
+ }, [
86
+ labelAnim,
87
+ theme.typography.bodyLarge.fontSize,
88
+ theme.typography.bodySmall.fontSize,
89
+ ])
90
+
91
+ // TranslateY: moves label from floated position down to resting position.
92
+ // Static top = floatedTop; translateY shifts it to restingTop when at rest.
93
+ const labelTranslateY = useMemo(() => {
94
+ const restingTop = isFilled
95
+ ? labelPositions.filledRestingTop
96
+ : labelPositions.outlinedRestingTop
97
+ const floatedTop = isFilled
98
+ ? labelPositions.filledFloatedTop
99
+ : labelPositions.outlinedFloatedTop
100
+ const restingOffset = restingTop - floatedTop
101
+ return labelAnim.interpolate({
102
+ inputRange: [0, 1],
103
+ outputRange: [restingOffset, 0],
104
+ })
105
+ }, [isFilled, labelAnim])
106
+
107
+ // Label start: 16dp container padding + leading icon space (12dp inset + 24dp + 16dp gap)
108
+ const labelStart =
109
+ theme.spacing.md + (hasLeadingIcon ? ICON_WITH_GAP - theme.spacing.md : 0)
110
+ // Static top = floated position; translateY handles resting offset
111
+ const labelStaticTop = isFilled
112
+ ? labelPositions.filledFloatedTop
113
+ : labelPositions.outlinedFloatedTop
114
+
115
+ const handleChangeText = useCallback(
116
+ (text: string) => {
117
+ if (!isControlled) {
118
+ setInternalHasText(text !== '')
119
+ }
120
+ onChangeText?.(text)
121
+ },
122
+ [isControlled, onChangeText],
123
+ )
124
+
125
+ const handleFocus = useCallback(
126
+ (event: NativeSyntheticEvent<TargetedEvent>) => {
127
+ if (isDisabled) return
128
+ setIsFocused(true)
129
+ onFocus?.(event)
130
+ },
131
+ [isDisabled, onFocus],
132
+ )
133
+
134
+ const handleBlur = useCallback(
135
+ (event: NativeSyntheticEvent<TargetedEvent>) => {
136
+ setIsFocused(false)
137
+ onBlur?.(event)
138
+ },
139
+ [onBlur],
140
+ )
141
+
142
+ const handleContainerPress = useCallback(() => {
143
+ if (!isDisabled) {
144
+ inputRef.current?.focus()
145
+ }
146
+ }, [isDisabled])
147
+
148
+ const labelColor = isDisabled
149
+ ? colors.disabledLabelColor
150
+ : isError
151
+ ? colors.errorLabelColor
152
+ : isFocused
153
+ ? colors.focusedLabelColor
154
+ : colors.labelColor
155
+
156
+ const labelBackgroundColor =
157
+ variant === 'outlined' && isLabelFloated
158
+ ? theme.colors.surface
159
+ : 'transparent'
160
+
161
+ const iconColor = isDisabled
162
+ ? colors.disabledIconColor
163
+ : isError
164
+ ? colors.errorIconColor
165
+ : contentColor ?? colors.iconColor
166
+
167
+ const containerStyle = useMemo(
168
+ () => [
169
+ styles.container,
170
+ containerColor && !isDisabled
171
+ ? { backgroundColor: containerColor }
172
+ : undefined,
173
+ isFocused && styles.containerFocused,
174
+ isError && !isFocused && styles.containerError,
175
+ isDisabled && styles.containerDisabled,
176
+ ],
177
+ [styles, isFocused, isError, isDisabled, containerColor],
178
+ )
179
+
180
+ const indicatorStyle = useMemo(
181
+ () => [
182
+ styles.indicator,
183
+ isFocused && styles.indicatorFocused,
184
+ isError && !isFocused && styles.indicatorError,
185
+ isDisabled && styles.indicatorDisabled,
186
+ ],
187
+ [styles, isFocused, isError, isDisabled],
188
+ )
189
+
190
+ const displaySupportingText = isError ? errorText : supportingText
191
+
192
+ return (
193
+ <View style={[styles.root, style]}>
194
+ <Pressable onPress={handleContainerPress} disabled={isDisabled}>
195
+ <View style={containerStyle}>
196
+ {leadingIcon ? (
197
+ <View style={styles.leadingIcon}>
198
+ <MaterialCommunityIcons
199
+ name={leadingIcon}
200
+ size={ICON_SIZE}
201
+ color={iconColor}
202
+ />
203
+ </View>
204
+ ) : null}
205
+
206
+ <View
207
+ style={[
208
+ styles.inputWrapper,
209
+ label ? styles.inputWrapperWithLabel : undefined,
210
+ ]}
211
+ >
212
+ <TextInput
213
+ ref={inputRef}
214
+ {...textInputProps}
215
+ value={value}
216
+ onChangeText={handleChangeText}
217
+ editable={!isDisabled}
218
+ onFocus={handleFocus}
219
+ onBlur={handleBlur}
220
+ placeholder={isLabelFloated || !label ? placeholder : undefined}
221
+ placeholderTextColor={colors.placeholderColor}
222
+ multiline={multiline}
223
+ style={[
224
+ styles.input,
225
+ isDisabled ? styles.inputDisabled : undefined,
226
+ contentColor && !isDisabled
227
+ ? { color: contentColor }
228
+ : undefined,
229
+ inputStyle,
230
+ ]}
231
+ accessibilityLabel={label || undefined}
232
+ accessibilityState={{ disabled: isDisabled }}
233
+ accessibilityHint={isError && errorText ? errorText : undefined}
234
+ />
235
+ </View>
236
+
237
+ {trailingIcon ? (
238
+ <Pressable
239
+ onPress={onTrailingIconPress}
240
+ disabled={isDisabled || !onTrailingIconPress}
241
+ accessibilityRole="button"
242
+ hitSlop={12}
243
+ style={styles.trailingIconPressable}
244
+ >
245
+ <View style={styles.trailingIcon}>
246
+ <MaterialCommunityIcons
247
+ name={trailingIcon}
248
+ size={ICON_SIZE}
249
+ color={iconColor}
250
+ />
251
+ </View>
252
+ </Pressable>
253
+ ) : null}
254
+
255
+ {/* Label: rendered at bodySmall, scaled up via transform when resting */}
256
+ {label ? (
257
+ <Animated.Text
258
+ numberOfLines={1}
259
+ style={[
260
+ styles.label,
261
+ {
262
+ top: labelStaticTop,
263
+ start: labelStart,
264
+ color: labelColor,
265
+ backgroundColor: labelBackgroundColor,
266
+ transform: [
267
+ { translateY: labelTranslateY },
268
+ { scale: labelScale },
269
+ ],
270
+ },
271
+ variant === 'outlined' && isLabelFloated
272
+ ? styles.labelNotch
273
+ : undefined,
274
+ ]}
275
+ >
276
+ {label}
277
+ </Animated.Text>
278
+ ) : null}
279
+
280
+ {isFilled ? <View style={indicatorStyle} /> : null}
281
+ </View>
282
+ </Pressable>
283
+
284
+ {displaySupportingText ? (
285
+ <View style={styles.supportingTextRow}>
286
+ <Text
287
+ style={[
288
+ styles.supportingText,
289
+ isError ? styles.errorSupportingText : undefined,
290
+ ]}
291
+ >
292
+ {displaySupportingText}
293
+ </Text>
294
+ </View>
295
+ ) : null}
296
+ </View>
297
+ )
298
+ }
@@ -0,0 +1,2 @@
1
+ export { TextField } from './TextField'
2
+ export type { TextFieldProps, TextFieldVariant } from './types'