rn-inputforge 0.1.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 (193) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/lib/commonjs/components/ChatInput/ChatInput.js +170 -0
  4. package/lib/commonjs/components/ChatInput/ChatInput.js.map +1 -0
  5. package/lib/commonjs/components/ChatInput/index.js +13 -0
  6. package/lib/commonjs/components/ChatInput/index.js.map +1 -0
  7. package/lib/commonjs/components/ChatInput/types.js +6 -0
  8. package/lib/commonjs/components/ChatInput/types.js.map +1 -0
  9. package/lib/commonjs/components/DescriptionField/DescriptionField.js +40 -0
  10. package/lib/commonjs/components/DescriptionField/DescriptionField.js.map +1 -0
  11. package/lib/commonjs/components/DescriptionField/index.js +13 -0
  12. package/lib/commonjs/components/DescriptionField/index.js.map +1 -0
  13. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js +207 -0
  14. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js.map +1 -0
  15. package/lib/commonjs/components/FieldFlowInput/index.js +13 -0
  16. package/lib/commonjs/components/FieldFlowInput/index.js.map +1 -0
  17. package/lib/commonjs/components/FieldFlowInput/styles.js +99 -0
  18. package/lib/commonjs/components/FieldFlowInput/styles.js.map +1 -0
  19. package/lib/commonjs/components/FieldFlowInput/types.js +6 -0
  20. package/lib/commonjs/components/FieldFlowInput/types.js.map +1 -0
  21. package/lib/commonjs/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.js +44 -0
  22. package/lib/commonjs/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.js.map +1 -0
  23. package/lib/commonjs/components/KeyboardAwareFormWrapper/index.js +13 -0
  24. package/lib/commonjs/components/KeyboardAwareFormWrapper/index.js.map +1 -0
  25. package/lib/commonjs/hooks/index.js +27 -0
  26. package/lib/commonjs/hooks/index.js.map +1 -0
  27. package/lib/commonjs/hooks/useFieldFlowForm.js +108 -0
  28. package/lib/commonjs/hooks/useFieldFlowForm.js.map +1 -0
  29. package/lib/commonjs/hooks/useFieldState.js +27 -0
  30. package/lib/commonjs/hooks/useFieldState.js.map +1 -0
  31. package/lib/commonjs/hooks/useKeyboardManager.js +68 -0
  32. package/lib/commonjs/hooks/useKeyboardManager.js.map +1 -0
  33. package/lib/commonjs/icons/Icon.js +388 -0
  34. package/lib/commonjs/icons/Icon.js.map +1 -0
  35. package/lib/commonjs/icons/index.js +13 -0
  36. package/lib/commonjs/icons/index.js.map +1 -0
  37. package/lib/commonjs/index.js +158 -0
  38. package/lib/commonjs/index.js.map +1 -0
  39. package/lib/commonjs/package.json +1 -0
  40. package/lib/commonjs/theme/ThemeProvider.js +48 -0
  41. package/lib/commonjs/theme/ThemeProvider.js.map +1 -0
  42. package/lib/commonjs/theme/defaultTheme.js +62 -0
  43. package/lib/commonjs/theme/defaultTheme.js.map +1 -0
  44. package/lib/commonjs/theme/index.js +38 -0
  45. package/lib/commonjs/theme/index.js.map +1 -0
  46. package/lib/commonjs/types/index.js +6 -0
  47. package/lib/commonjs/types/index.js.map +1 -0
  48. package/lib/commonjs/utils/formatters.js +25 -0
  49. package/lib/commonjs/utils/formatters.js.map +1 -0
  50. package/lib/commonjs/utils/keyboardTypes.js +103 -0
  51. package/lib/commonjs/utils/keyboardTypes.js.map +1 -0
  52. package/lib/commonjs/validation/index.js +80 -0
  53. package/lib/commonjs/validation/index.js.map +1 -0
  54. package/lib/commonjs/validation/useValidation.js +34 -0
  55. package/lib/commonjs/validation/useValidation.js.map +1 -0
  56. package/lib/commonjs/validation/validators.js +118 -0
  57. package/lib/commonjs/validation/validators.js.map +1 -0
  58. package/lib/module/components/ChatInput/ChatInput.js +165 -0
  59. package/lib/module/components/ChatInput/ChatInput.js.map +1 -0
  60. package/lib/module/components/ChatInput/index.js +4 -0
  61. package/lib/module/components/ChatInput/index.js.map +1 -0
  62. package/lib/module/components/ChatInput/types.js +4 -0
  63. package/lib/module/components/ChatInput/types.js.map +1 -0
  64. package/lib/module/components/DescriptionField/DescriptionField.js +35 -0
  65. package/lib/module/components/DescriptionField/DescriptionField.js.map +1 -0
  66. package/lib/module/components/DescriptionField/index.js +4 -0
  67. package/lib/module/components/DescriptionField/index.js.map +1 -0
  68. package/lib/module/components/FieldFlowInput/FieldFlowInput.js +202 -0
  69. package/lib/module/components/FieldFlowInput/FieldFlowInput.js.map +1 -0
  70. package/lib/module/components/FieldFlowInput/index.js +4 -0
  71. package/lib/module/components/FieldFlowInput/index.js.map +1 -0
  72. package/lib/module/components/FieldFlowInput/styles.js +92 -0
  73. package/lib/module/components/FieldFlowInput/styles.js.map +1 -0
  74. package/lib/module/components/FieldFlowInput/types.js +4 -0
  75. package/lib/module/components/FieldFlowInput/types.js.map +1 -0
  76. package/lib/module/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.js +39 -0
  77. package/lib/module/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.js.map +1 -0
  78. package/lib/module/components/KeyboardAwareFormWrapper/index.js +4 -0
  79. package/lib/module/components/KeyboardAwareFormWrapper/index.js.map +1 -0
  80. package/lib/module/hooks/index.js +6 -0
  81. package/lib/module/hooks/index.js.map +1 -0
  82. package/lib/module/hooks/useFieldFlowForm.js +104 -0
  83. package/lib/module/hooks/useFieldFlowForm.js.map +1 -0
  84. package/lib/module/hooks/useFieldState.js +23 -0
  85. package/lib/module/hooks/useFieldState.js.map +1 -0
  86. package/lib/module/hooks/useKeyboardManager.js +64 -0
  87. package/lib/module/hooks/useKeyboardManager.js.map +1 -0
  88. package/lib/module/icons/Icon.js +383 -0
  89. package/lib/module/icons/Icon.js.map +1 -0
  90. package/lib/module/icons/index.js +4 -0
  91. package/lib/module/icons/index.js.map +1 -0
  92. package/lib/module/index.js +18 -0
  93. package/lib/module/index.js.map +1 -0
  94. package/lib/module/theme/ThemeProvider.js +42 -0
  95. package/lib/module/theme/ThemeProvider.js.map +1 -0
  96. package/lib/module/theme/defaultTheme.js +58 -0
  97. package/lib/module/theme/defaultTheme.js.map +1 -0
  98. package/lib/module/theme/index.js +5 -0
  99. package/lib/module/theme/index.js.map +1 -0
  100. package/lib/module/types/index.js +4 -0
  101. package/lib/module/types/index.js.map +1 -0
  102. package/lib/module/utils/formatters.js +19 -0
  103. package/lib/module/utils/formatters.js.map +1 -0
  104. package/lib/module/utils/keyboardTypes.js +98 -0
  105. package/lib/module/utils/keyboardTypes.js.map +1 -0
  106. package/lib/module/validation/index.js +5 -0
  107. package/lib/module/validation/index.js.map +1 -0
  108. package/lib/module/validation/useValidation.js +30 -0
  109. package/lib/module/validation/useValidation.js.map +1 -0
  110. package/lib/module/validation/validators.js +103 -0
  111. package/lib/module/validation/validators.js.map +1 -0
  112. package/lib/typescript/components/ChatInput/ChatInput.d.ts +10 -0
  113. package/lib/typescript/components/ChatInput/ChatInput.d.ts.map +1 -0
  114. package/lib/typescript/components/ChatInput/index.d.ts +3 -0
  115. package/lib/typescript/components/ChatInput/index.d.ts.map +1 -0
  116. package/lib/typescript/components/ChatInput/types.d.ts +30 -0
  117. package/lib/typescript/components/ChatInput/types.d.ts.map +1 -0
  118. package/lib/typescript/components/DescriptionField/DescriptionField.d.ts +16 -0
  119. package/lib/typescript/components/DescriptionField/DescriptionField.d.ts.map +1 -0
  120. package/lib/typescript/components/DescriptionField/index.d.ts +3 -0
  121. package/lib/typescript/components/DescriptionField/index.d.ts.map +1 -0
  122. package/lib/typescript/components/FieldFlowInput/FieldFlowInput.d.ts +5 -0
  123. package/lib/typescript/components/FieldFlowInput/FieldFlowInput.d.ts.map +1 -0
  124. package/lib/typescript/components/FieldFlowInput/index.d.ts +3 -0
  125. package/lib/typescript/components/FieldFlowInput/index.d.ts.map +1 -0
  126. package/lib/typescript/components/FieldFlowInput/styles.d.ts +71 -0
  127. package/lib/typescript/components/FieldFlowInput/styles.d.ts.map +1 -0
  128. package/lib/typescript/components/FieldFlowInput/types.d.ts +50 -0
  129. package/lib/typescript/components/FieldFlowInput/types.d.ts.map +1 -0
  130. package/lib/typescript/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.d.ts +17 -0
  131. package/lib/typescript/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.d.ts.map +1 -0
  132. package/lib/typescript/components/KeyboardAwareFormWrapper/index.d.ts +3 -0
  133. package/lib/typescript/components/KeyboardAwareFormWrapper/index.d.ts.map +1 -0
  134. package/lib/typescript/hooks/index.d.ts +7 -0
  135. package/lib/typescript/hooks/index.d.ts.map +1 -0
  136. package/lib/typescript/hooks/useFieldFlowForm.d.ts +31 -0
  137. package/lib/typescript/hooks/useFieldFlowForm.d.ts.map +1 -0
  138. package/lib/typescript/hooks/useFieldState.d.ts +12 -0
  139. package/lib/typescript/hooks/useFieldState.d.ts.map +1 -0
  140. package/lib/typescript/hooks/useKeyboardManager.d.ts +35 -0
  141. package/lib/typescript/hooks/useKeyboardManager.d.ts.map +1 -0
  142. package/lib/typescript/icons/Icon.d.ts +17 -0
  143. package/lib/typescript/icons/Icon.d.ts.map +1 -0
  144. package/lib/typescript/icons/index.d.ts +3 -0
  145. package/lib/typescript/icons/index.d.ts.map +1 -0
  146. package/lib/typescript/index.d.ts +18 -0
  147. package/lib/typescript/index.d.ts.map +1 -0
  148. package/lib/typescript/theme/ThemeProvider.d.ts +16 -0
  149. package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
  150. package/lib/typescript/theme/defaultTheme.d.ts +4 -0
  151. package/lib/typescript/theme/defaultTheme.d.ts.map +1 -0
  152. package/lib/typescript/theme/index.d.ts +4 -0
  153. package/lib/typescript/theme/index.d.ts.map +1 -0
  154. package/lib/typescript/types/index.d.ts +79 -0
  155. package/lib/typescript/types/index.d.ts.map +1 -0
  156. package/lib/typescript/utils/formatters.d.ts +5 -0
  157. package/lib/typescript/utils/formatters.d.ts.map +1 -0
  158. package/lib/typescript/utils/keyboardTypes.d.ts +18 -0
  159. package/lib/typescript/utils/keyboardTypes.d.ts.map +1 -0
  160. package/lib/typescript/validation/index.d.ts +4 -0
  161. package/lib/typescript/validation/index.d.ts.map +1 -0
  162. package/lib/typescript/validation/useValidation.d.ts +14 -0
  163. package/lib/typescript/validation/useValidation.d.ts.map +1 -0
  164. package/lib/typescript/validation/validators.d.ts +17 -0
  165. package/lib/typescript/validation/validators.d.ts.map +1 -0
  166. package/package.json +80 -0
  167. package/src/components/ChatInput/ChatInput.tsx +178 -0
  168. package/src/components/ChatInput/index.ts +2 -0
  169. package/src/components/ChatInput/types.ts +35 -0
  170. package/src/components/DescriptionField/DescriptionField.tsx +39 -0
  171. package/src/components/DescriptionField/index.ts +2 -0
  172. package/src/components/FieldFlowInput/FieldFlowInput.tsx +240 -0
  173. package/src/components/FieldFlowInput/index.ts +2 -0
  174. package/src/components/FieldFlowInput/styles.ts +94 -0
  175. package/src/components/FieldFlowInput/types.ts +55 -0
  176. package/src/components/KeyboardAwareFormWrapper/KeyboardAwareFormWrapper.tsx +45 -0
  177. package/src/components/KeyboardAwareFormWrapper/index.ts +2 -0
  178. package/src/hooks/index.ts +12 -0
  179. package/src/hooks/useFieldFlowForm.ts +133 -0
  180. package/src/hooks/useFieldState.ts +31 -0
  181. package/src/hooks/useKeyboardManager.ts +105 -0
  182. package/src/icons/Icon.tsx +338 -0
  183. package/src/icons/index.ts +2 -0
  184. package/src/index.ts +63 -0
  185. package/src/theme/ThemeProvider.tsx +49 -0
  186. package/src/theme/defaultTheme.ts +58 -0
  187. package/src/theme/index.ts +3 -0
  188. package/src/types/index.ts +98 -0
  189. package/src/utils/formatters.ts +18 -0
  190. package/src/utils/keyboardTypes.ts +85 -0
  191. package/src/validation/index.ts +15 -0
  192. package/src/validation/useValidation.ts +37 -0
  193. package/src/validation/validators.ts +93 -0
@@ -0,0 +1,178 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ NativeSyntheticEvent,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ TextInput,
9
+ TextInputContentSizeChangeEventData,
10
+ View,
11
+ } from 'react-native';
12
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
13
+ import { defaultTheme } from '../../theme/defaultTheme';
14
+ import { FieldFlowIcon } from '../../icons/Icon';
15
+ import type { ChatAttachmentAction, ChatInputProps } from './types';
16
+
17
+ const ACTION_ICON: Record<ChatAttachmentAction, Parameters<typeof FieldFlowIcon>[0]['name']> = {
18
+ camera: 'camera',
19
+ gallery: 'image',
20
+ document: 'paperclip',
21
+ location: 'mapPin',
22
+ voice: 'mic',
23
+ };
24
+
25
+ /**
26
+ * A ready-to-drop-in chat composer for messaging screens: expandable input,
27
+ * a configurable row of attachment buttons (camera / gallery / document /
28
+ * location / voice, plus your own custom actions), and a send button that
29
+ * only lights up once there's something to send.
30
+ */
31
+ export function ChatInput({
32
+ value,
33
+ defaultValue,
34
+ onChangeText,
35
+ onSend,
36
+ placeholder = 'Message',
37
+ attachmentActions = [],
38
+ onAttachmentPress,
39
+ customActions = [],
40
+ disabled,
41
+ loading,
42
+ maxLength,
43
+ minHeight = 40,
44
+ maxHeight = 140,
45
+ theme: themeOverride,
46
+ testID,
47
+ }: ChatInputProps) {
48
+ const contextTheme = useFieldFlowTheme();
49
+ const theme = useMemo(
50
+ () => (themeOverride ? mergeDeep(contextTheme ?? defaultTheme, themeOverride) : contextTheme ?? defaultTheme),
51
+ [contextTheme, themeOverride],
52
+ );
53
+
54
+ const isControlled = value !== undefined;
55
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '');
56
+ const currentValue = isControlled ? value ?? '' : internalValue;
57
+ const [height, setHeight] = useState(minHeight);
58
+ const inputRef = useRef<TextInput>(null);
59
+
60
+ const handleChangeText = (next: string) => {
61
+ if (!isControlled) setInternalValue(next);
62
+ onChangeText?.(next);
63
+ };
64
+
65
+ const handleContentSizeChange = (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
66
+ setHeight(Math.min(maxHeight, Math.max(minHeight, e.nativeEvent.contentSize.height)));
67
+ };
68
+
69
+ const handleSend = () => {
70
+ const trimmed = currentValue.trim();
71
+ if (!trimmed || disabled || loading) return;
72
+ onSend(trimmed);
73
+ if (!isControlled) setInternalValue('');
74
+ inputRef.current?.clear();
75
+ };
76
+
77
+ const canSend = currentValue.trim().length > 0 && !disabled && !loading;
78
+
79
+ const styles = useMemo(() => createStyles(theme), [theme]);
80
+
81
+ return (
82
+ <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} testID={testID}>
83
+ <View style={styles.container}>
84
+ {attachmentActions.length > 0 || customActions.length > 0 ? (
85
+ <View style={styles.toolbar}>
86
+ {attachmentActions.map(action => (
87
+ <Pressable
88
+ key={action}
89
+ onPress={() => onAttachmentPress?.(action)}
90
+ style={styles.toolbarButton}
91
+ hitSlop={8}
92
+ disabled={disabled}
93
+ >
94
+ <FieldFlowIcon name={ACTION_ICON[action]} color={theme.colors.icon} size={20} />
95
+ </Pressable>
96
+ ))}
97
+ {customActions.map(action => (
98
+ <Pressable key={action.id} onPress={action.onPress} style={styles.toolbarButton} hitSlop={8}>
99
+ {typeof action.icon === 'string' ? (
100
+ <FieldFlowIcon name={action.icon as any} color={theme.colors.icon} size={20} />
101
+ ) : (
102
+ action.icon
103
+ )}
104
+ </Pressable>
105
+ ))}
106
+ </View>
107
+ ) : null}
108
+
109
+ <View style={styles.inputRow}>
110
+ <TextInput
111
+ ref={inputRef}
112
+ value={currentValue}
113
+ onChangeText={handleChangeText}
114
+ onContentSizeChange={handleContentSizeChange}
115
+ placeholder={placeholder}
116
+ placeholderTextColor={theme.colors.placeholder}
117
+ multiline
118
+ maxLength={maxLength}
119
+ editable={!disabled && !loading}
120
+ style={[styles.input, { height }]}
121
+ />
122
+ <Pressable
123
+ onPress={handleSend}
124
+ disabled={!canSend}
125
+ style={[styles.sendButton, { opacity: canSend ? 1 : 0.4 }]}
126
+ hitSlop={8}
127
+ >
128
+ <FieldFlowIcon name={loading ? 'spinner' : 'send'} color="#FFFFFF" size={16} />
129
+ </Pressable>
130
+ </View>
131
+ </View>
132
+ </KeyboardAvoidingView>
133
+ );
134
+ }
135
+
136
+ function createStyles(theme: ReturnType<typeof useFieldFlowTheme>) {
137
+ return StyleSheet.create({
138
+ container: {
139
+ backgroundColor: theme.colors.background,
140
+ borderTopWidth: theme.spacing.borderWidth,
141
+ borderTopColor: theme.colors.border,
142
+ paddingHorizontal: theme.spacing.paddingHorizontal,
143
+ paddingVertical: theme.spacing.gap,
144
+ },
145
+ toolbar: {
146
+ flexDirection: 'row',
147
+ marginBottom: theme.spacing.gap,
148
+ },
149
+ toolbarButton: {
150
+ marginRight: theme.spacing.gap * 2,
151
+ alignItems: 'center',
152
+ justifyContent: 'center',
153
+ },
154
+ inputRow: {
155
+ flexDirection: 'row',
156
+ alignItems: 'flex-end',
157
+ },
158
+ input: {
159
+ flex: 1,
160
+ fontSize: theme.typography.fontSize,
161
+ color: theme.colors.text,
162
+ backgroundColor: theme.colors.backgroundDisabled,
163
+ borderRadius: theme.spacing.borderRadius * 1.6,
164
+ paddingHorizontal: theme.spacing.paddingHorizontal,
165
+ paddingVertical: theme.spacing.paddingVertical * 0.7,
166
+ textAlignVertical: 'top',
167
+ },
168
+ sendButton: {
169
+ width: 34,
170
+ height: 34,
171
+ borderRadius: 17,
172
+ backgroundColor: theme.colors.borderFocused,
173
+ alignItems: 'center',
174
+ justifyContent: 'center',
175
+ marginLeft: theme.spacing.gap,
176
+ },
177
+ });
178
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatInput } from './ChatInput';
2
+ export type { ChatInputProps, ChatAttachmentAction, CustomChatAction } from './types';
@@ -0,0 +1,35 @@
1
+ import type React from 'react';
2
+ import type { DeepPartial, FieldFlowTheme } from '../../types';
3
+ import type { FieldFlowIconName } from '../../icons';
4
+
5
+ export type ChatAttachmentAction = 'camera' | 'gallery' | 'document' | 'location' | 'voice';
6
+
7
+ export interface CustomChatAction {
8
+ id: string;
9
+ icon: FieldFlowIconName | React.ReactNode;
10
+ label?: string;
11
+ onPress: () => void;
12
+ }
13
+
14
+ export interface ChatInputProps {
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onChangeText?: (value: string) => void;
18
+ onSend: (message: string) => void;
19
+ placeholder?: string;
20
+
21
+ /** Which built-in attachment buttons to show in the toolbar, in order. */
22
+ attachmentActions?: ChatAttachmentAction[];
23
+ onAttachmentPress?: (action: ChatAttachmentAction) => void;
24
+ /** Extra app-specific buttons appended after the built-in ones. */
25
+ customActions?: CustomChatAction[];
26
+
27
+ disabled?: boolean;
28
+ loading?: boolean;
29
+ maxLength?: number;
30
+ minHeight?: number;
31
+ maxHeight?: number;
32
+
33
+ theme?: DeepPartial<FieldFlowTheme>;
34
+ testID?: string;
35
+ }
@@ -0,0 +1,39 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import type { NativeSyntheticEvent, TextInput, TextInputContentSizeChangeEventData } from 'react-native';
3
+ import { FieldFlowInput } from '../FieldFlowInput/FieldFlowInput';
4
+ import type { FieldFlowInputProps } from '../FieldFlowInput/types';
5
+
6
+ export interface DescriptionFieldProps extends Omit<FieldFlowInputProps, 'type'> {
7
+ /** Minimum height in px before the field starts growing. Default 80. */
8
+ minHeight?: number;
9
+ /** Maximum height in px the field will grow to before scrolling internally. Default 200. */
10
+ maxHeight?: number;
11
+ }
12
+
13
+ /**
14
+ * A dedicated multi-line field for comments, notes, descriptions, and feedback.
15
+ * Grows automatically as the user types, up to `maxHeight`, and supports the
16
+ * same character-limit / validation / theming props as FieldFlowInput.
17
+ */
18
+ export const DescriptionField = forwardRef<TextInput, DescriptionFieldProps>(function DescriptionField(
19
+ { minHeight = 80, maxHeight = 200, inputStyle, showCharacterCount = true, ...rest },
20
+ ref,
21
+ ) {
22
+ const [height, setHeight] = useState(minHeight);
23
+
24
+ const handleContentSizeChange = (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
25
+ const next = Math.min(maxHeight, Math.max(minHeight, e.nativeEvent.contentSize.height));
26
+ setHeight(next);
27
+ };
28
+
29
+ return (
30
+ <FieldFlowInput
31
+ ref={ref}
32
+ type="description"
33
+ showCharacterCount={showCharacterCount}
34
+ inputStyle={{ height, ...inputStyle }}
35
+ onContentSizeChange={handleContentSizeChange}
36
+ {...rest}
37
+ />
38
+ );
39
+ });
@@ -0,0 +1,2 @@
1
+ export { DescriptionField } from './DescriptionField';
2
+ export type { DescriptionFieldProps } from './DescriptionField';
@@ -0,0 +1,240 @@
1
+ import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
+ import { Pressable, Text, TextInput, View } from 'react-native';
3
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
4
+ import { defaultTheme } from '../../theme/defaultTheme';
5
+ import { useFieldState } from '../../hooks/useFieldState';
6
+ import { resolveFieldConfig, implicitRulesForType } from '../../utils/keyboardTypes';
7
+ import { sanitizeNumeric, sanitizePhone, truncate } from '../../utils/formatters';
8
+ import { runRules } from '../../validation/validators';
9
+ import { FieldFlowIcon } from '../../icons/Icon';
10
+ import { createStyles, iconColorFor } from './styles';
11
+ import type { FieldFlowInputProps } from './types';
12
+
13
+ function renderIconNode(
14
+ icon: FieldFlowInputProps['leftIcon'],
15
+ color: string,
16
+ ): React.ReactNode {
17
+ if (!icon) return null;
18
+ if (typeof icon === 'string') {
19
+ return <FieldFlowIcon name={icon as any} color={color} size={18} />;
20
+ }
21
+ return icon;
22
+ }
23
+
24
+ export const FieldFlowInput = forwardRef<TextInput, FieldFlowInputProps>(function FieldFlowInput(
25
+ {
26
+ type = 'text',
27
+ label,
28
+ helperText,
29
+ errorText,
30
+ hasError,
31
+ successText,
32
+ isSuccess,
33
+ loading,
34
+ disabled,
35
+ readOnly,
36
+ required,
37
+ value,
38
+ defaultValue,
39
+ onChangeText,
40
+ onFocus,
41
+ onBlur,
42
+ rules,
43
+ validateOnChange,
44
+ onValidate,
45
+ leftIcon,
46
+ rightIcon,
47
+ onLeftIconPress,
48
+ onRightIconPress,
49
+ clearable,
50
+ maxLength,
51
+ showCharacterCount,
52
+ onSubmitEditing,
53
+ containerStyle,
54
+ inputStyle,
55
+ labelStyle,
56
+ helperStyle,
57
+ theme: themeOverride,
58
+ testID,
59
+ ...rest
60
+ },
61
+ forwardedRef,
62
+ ) {
63
+ const contextTheme = useFieldFlowTheme();
64
+ const theme = useMemo(
65
+ () => (themeOverride ? mergeDeep(contextTheme ?? defaultTheme, themeOverride) : contextTheme ?? defaultTheme),
66
+ [contextTheme, themeOverride],
67
+ );
68
+
69
+ const innerRef = useRef<TextInput>(null);
70
+ useImperativeHandle(forwardedRef, () => innerRef.current as TextInput);
71
+
72
+ const isControlled = value !== undefined;
73
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '');
74
+ const currentValue = isControlled ? value ?? '' : internalValue;
75
+
76
+ const [isFocused, setIsFocused] = useState(false);
77
+ const [showPassword, setShowPassword] = useState(false);
78
+ const [localError, setLocalError] = useState<string | undefined>(undefined);
79
+
80
+ const fieldConfig = useMemo(() => resolveFieldConfig(type), [type]);
81
+ const effectiveRules = useMemo(
82
+ () => ({ ...implicitRulesForType(type), ...(required ? { required: true } : {}), ...rules }),
83
+ [type, required, rules],
84
+ );
85
+
86
+ const resolvedError = errorText ?? localError;
87
+ const state = useFieldState({
88
+ isFocused,
89
+ hasError: Boolean(hasError || resolvedError),
90
+ isSuccess,
91
+ disabled,
92
+ readOnly,
93
+ loading,
94
+ });
95
+
96
+ const styles = useMemo(
97
+ () => createStyles(theme, state, fieldConfig.multiline && type !== 'chat'),
98
+ [theme, state, fieldConfig.multiline, type],
99
+ );
100
+ const iconColor = iconColorFor(theme, state);
101
+
102
+ const runValidation = useCallback(
103
+ (nextValue: string) => {
104
+ const result = runRules(nextValue, effectiveRules);
105
+ setLocalError(result.valid ? undefined : result.message);
106
+ onValidate?.(result);
107
+ return result.valid;
108
+ },
109
+ [effectiveRules, onValidate],
110
+ );
111
+
112
+ const handleChangeText = useCallback(
113
+ (raw: string) => {
114
+ let next = raw;
115
+ if (type === 'number') next = sanitizeNumeric(next, false);
116
+ if (type === 'decimal') next = sanitizeNumeric(next, true);
117
+ if (type === 'phone') next = sanitizePhone(next);
118
+ next = truncate(next, maxLength);
119
+
120
+ if (!isControlled) setInternalValue(next);
121
+ onChangeText?.(next);
122
+ if (validateOnChange) runValidation(next);
123
+ },
124
+ [type, maxLength, isControlled, onChangeText, validateOnChange, runValidation],
125
+ );
126
+
127
+ const handleFocus = useCallback(() => {
128
+ setIsFocused(true);
129
+ onFocus?.();
130
+ }, [onFocus]);
131
+
132
+ const handleBlur = useCallback(() => {
133
+ setIsFocused(false);
134
+ runValidation(currentValue);
135
+ onBlur?.();
136
+ }, [runValidation, currentValue, onBlur]);
137
+
138
+ const handleClear = useCallback(() => {
139
+ if (!isControlled) setInternalValue('');
140
+ onChangeText?.('');
141
+ innerRef.current?.focus();
142
+ }, [isControlled, onChangeText]);
143
+
144
+ const isClearable = clearable ?? type === 'search';
145
+ const showClearButton = isClearable && currentValue.length > 0 && !disabled && !readOnly && !loading;
146
+
147
+ const resolvedLeftIcon = leftIcon ?? (type === 'search' ? 'search' : undefined);
148
+ let resolvedRightIcon = rightIcon;
149
+ let rightIconPress = onRightIconPress;
150
+
151
+ if (!resolvedRightIcon) {
152
+ if (loading) {
153
+ resolvedRightIcon = 'spinner';
154
+ } else if (type === 'password') {
155
+ resolvedRightIcon = showPassword ? 'eyeOff' : 'eye';
156
+ rightIconPress = () => setShowPassword(v => !v);
157
+ } else if (showClearButton) {
158
+ resolvedRightIcon = 'clear';
159
+ rightIconPress = handleClear;
160
+ } else if (state === 'error') {
161
+ resolvedRightIcon = 'alert';
162
+ } else if (state === 'success') {
163
+ resolvedRightIcon = 'check';
164
+ }
165
+ }
166
+
167
+ return (
168
+ <View style={[styles.wrapper, containerStyle]} testID={testID}>
169
+ {label ? (
170
+ <Text style={[styles.label, labelStyle]}>
171
+ {label}
172
+ {required ? ' *' : ''}
173
+ </Text>
174
+ ) : null}
175
+
176
+ <View style={styles.inputContainer}>
177
+ {resolvedLeftIcon ? (
178
+ <Pressable
179
+ onPress={onLeftIconPress}
180
+ disabled={!onLeftIconPress}
181
+ style={[styles.iconSlot, styles.iconSlotLeft]}
182
+ hitSlop={8}
183
+ >
184
+ {renderIconNode(resolvedLeftIcon, iconColor)}
185
+ </Pressable>
186
+ ) : null}
187
+
188
+ <TextInput
189
+ ref={innerRef}
190
+ value={currentValue}
191
+ onChangeText={handleChangeText}
192
+ onFocus={handleFocus}
193
+ onBlur={handleBlur}
194
+ onSubmitEditing={onSubmitEditing}
195
+ editable={!disabled && !readOnly && !loading}
196
+ placeholderTextColor={theme.colors.placeholder}
197
+ keyboardType={fieldConfig.keyboardType}
198
+ secureTextEntry={fieldConfig.secureTextEntry && !showPassword}
199
+ autoCapitalize={fieldConfig.autoCapitalize}
200
+ autoCorrect={fieldConfig.autoCorrect}
201
+ multiline={fieldConfig.multiline && type !== 'chat'}
202
+ maxLength={maxLength}
203
+ returnKeyType={fieldConfig.multiline ? 'default' : 'done'}
204
+ style={[styles.input, inputStyle]}
205
+ {...rest}
206
+ />
207
+
208
+ {resolvedRightIcon ? (
209
+ <Pressable
210
+ onPress={rightIconPress}
211
+ disabled={!rightIconPress}
212
+ style={[styles.iconSlot, styles.iconSlotRight]}
213
+ hitSlop={8}
214
+ >
215
+ {renderIconNode(resolvedRightIcon, iconColor)}
216
+ </Pressable>
217
+ ) : null}
218
+ </View>
219
+
220
+ {(helperText || resolvedError || successText || (showCharacterCount && maxLength)) && (
221
+ <View style={styles.footerRow}>
222
+ <View style={{ flex: 1 }}>
223
+ {resolvedError ? (
224
+ <Text style={styles.errorText}>{resolvedError}</Text>
225
+ ) : successText ? (
226
+ <Text style={styles.successText}>{successText}</Text>
227
+ ) : helperText ? (
228
+ <Text style={styles.helperText}>{helperText}</Text>
229
+ ) : null}
230
+ </View>
231
+ {showCharacterCount && maxLength ? (
232
+ <Text style={styles.charCount}>
233
+ {currentValue.length}/{maxLength}
234
+ </Text>
235
+ ) : null}
236
+ </View>
237
+ )}
238
+ </View>
239
+ );
240
+ });
@@ -0,0 +1,2 @@
1
+ export { FieldFlowInput } from './FieldFlowInput';
2
+ export type { FieldFlowInputProps } from './types';
@@ -0,0 +1,94 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import type { FieldFlowState, FieldFlowTheme } from '../../types';
3
+
4
+ export function borderColorFor(theme: FieldFlowTheme, state: FieldFlowState): string {
5
+ switch (state) {
6
+ case 'focused':
7
+ return theme.colors.borderFocused;
8
+ case 'error':
9
+ return theme.colors.borderError;
10
+ case 'success':
11
+ return theme.colors.borderSuccess;
12
+ default:
13
+ return theme.colors.border;
14
+ }
15
+ }
16
+
17
+ export function labelColorFor(theme: FieldFlowTheme, state: FieldFlowState): string {
18
+ if (state === 'error') return theme.colors.error;
19
+ if (state === 'focused') return theme.colors.labelFocused;
20
+ return theme.colors.label;
21
+ }
22
+
23
+ export function iconColorFor(theme: FieldFlowTheme, state: FieldFlowState): string {
24
+ return state === 'focused' ? theme.colors.iconFocused : theme.colors.icon;
25
+ }
26
+
27
+ export function createStyles(theme: FieldFlowTheme, state: FieldFlowState, multiline: boolean) {
28
+ return StyleSheet.create({
29
+ wrapper: {
30
+ marginBottom: theme.spacing.gap * 2,
31
+ },
32
+ label: {
33
+ fontSize: theme.typography.labelFontSize,
34
+ marginBottom: theme.spacing.gap / 1.5,
35
+ color: labelColorFor(theme, state),
36
+ fontWeight: '500',
37
+ },
38
+ inputContainer: {
39
+ flexDirection: 'row',
40
+ alignItems: multiline ? 'flex-start' : 'center',
41
+ backgroundColor: state === 'disabled' ? theme.colors.backgroundDisabled : theme.colors.background,
42
+ borderRadius: theme.spacing.borderRadius,
43
+ borderWidth: state === 'focused' ? theme.spacing.borderWidthFocused : theme.spacing.borderWidth,
44
+ borderColor: borderColorFor(theme, state),
45
+ paddingHorizontal: theme.spacing.paddingHorizontal,
46
+ paddingVertical: multiline ? theme.spacing.paddingVertical : 0,
47
+ minHeight: multiline ? undefined : theme.spacing.paddingVertical * 2 + theme.typography.fontSize + 6,
48
+ },
49
+ input: {
50
+ flex: 1,
51
+ fontSize: theme.typography.fontSize,
52
+ color: theme.colors.text,
53
+ paddingVertical: multiline ? 0 : theme.spacing.paddingVertical,
54
+ textAlignVertical: multiline ? 'top' : 'center',
55
+ minHeight: multiline ? 80 : undefined,
56
+ },
57
+ iconSlot: {
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ marginTop: multiline ? theme.spacing.paddingVertical : 0,
61
+ },
62
+ iconSlotLeft: {
63
+ marginRight: theme.spacing.gap,
64
+ },
65
+ iconSlotRight: {
66
+ marginLeft: theme.spacing.gap,
67
+ },
68
+ footerRow: {
69
+ flexDirection: 'row',
70
+ justifyContent: 'space-between',
71
+ marginTop: theme.spacing.gap / 1.5,
72
+ },
73
+ helperText: {
74
+ fontSize: theme.typography.helperFontSize,
75
+ color: theme.colors.label,
76
+ flexShrink: 1,
77
+ },
78
+ errorText: {
79
+ fontSize: theme.typography.helperFontSize,
80
+ color: theme.colors.error,
81
+ flexShrink: 1,
82
+ },
83
+ successText: {
84
+ fontSize: theme.typography.helperFontSize,
85
+ color: theme.colors.success,
86
+ flexShrink: 1,
87
+ },
88
+ charCount: {
89
+ fontSize: theme.typography.helperFontSize,
90
+ color: theme.colors.placeholder,
91
+ marginLeft: theme.spacing.gap,
92
+ },
93
+ });
94
+ }
@@ -0,0 +1,55 @@
1
+ import type React from 'react';
2
+ import type { TextInputProps, ViewStyle, TextStyle } from 'react-native';
3
+ import type { FieldFlowFieldType, FieldFlowRules, DeepPartial, FieldFlowTheme } from '../../types';
4
+ import type { FieldFlowIconName } from '../../icons';
5
+
6
+ export interface FieldFlowInputProps
7
+ extends Omit<TextInputProps, 'style' | 'onChangeText' | 'onFocus' | 'onBlur' | 'onSubmitEditing'> {
8
+ /** Determines keyboard, secure entry, validation defaults, and default icon behavior. */
9
+ type?: FieldFlowFieldType;
10
+ label?: string;
11
+ helperText?: string;
12
+ errorText?: string;
13
+ /** Set true to force the error visual state even without `errorText` (e.g. from a form hook). */
14
+ hasError?: boolean;
15
+ successText?: string;
16
+ isSuccess?: boolean;
17
+ loading?: boolean;
18
+ disabled?: boolean;
19
+ readOnly?: boolean;
20
+ required?: boolean;
21
+
22
+ value?: string;
23
+ defaultValue?: string;
24
+ onChangeText?: (value: string) => void;
25
+ onFocus?: () => void;
26
+ onBlur?: () => void;
27
+
28
+ /** Validation rules; if omitted, sensible defaults are inferred from `type` (e.g. email format). */
29
+ rules?: FieldFlowRules;
30
+ /** Runs validation on every keystroke instead of just on blur. */
31
+ validateOnChange?: boolean;
32
+ /** Called with the validation result whenever a validation pass runs. */
33
+ onValidate?: (result: { valid: boolean; message?: string }) => void;
34
+
35
+ leftIcon?: FieldFlowIconName | React.ReactNode;
36
+ rightIcon?: FieldFlowIconName | React.ReactNode;
37
+ onLeftIconPress?: () => void;
38
+ onRightIconPress?: () => void;
39
+ /** Shows an "x" clear button when there's text (defaults to true for `search`, false otherwise). */
40
+ clearable?: boolean;
41
+
42
+ maxLength?: number;
43
+ showCharacterCount?: boolean;
44
+
45
+ /** Return-key handling used by useKeyboardManager; if provided, overrides default onSubmitEditing wiring. */
46
+ onSubmitEditing?: () => void;
47
+
48
+ containerStyle?: ViewStyle;
49
+ inputStyle?: TextStyle;
50
+ labelStyle?: TextStyle;
51
+ helperStyle?: TextStyle;
52
+ theme?: DeepPartial<FieldFlowTheme>;
53
+
54
+ testID?: string;
55
+ }