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,45 @@
1
+ import React from 'react';
2
+ import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, ViewStyle } from 'react-native';
3
+ import type { UseKeyboardManagerResult } from '../../hooks/useKeyboardManager';
4
+
5
+ export interface KeyboardAwareFormWrapperProps {
6
+ keyboardManager: Pick<UseKeyboardManagerResult, 'scrollViewRef'>;
7
+ children: React.ReactNode;
8
+ contentContainerStyle?: ViewStyle;
9
+ style?: ViewStyle;
10
+ }
11
+
12
+ /**
13
+ * Wrap your form fields in this instead of a plain ScrollView. It automatically
14
+ * keeps the focused field visible above the keyboard and animates smoothly on
15
+ * both iOS and Android; pass it the same `keyboardManager` you got from
16
+ * `useKeyboardManager()`.
17
+ */
18
+ export function KeyboardAwareFormWrapper({
19
+ keyboardManager,
20
+ children,
21
+ contentContainerStyle,
22
+ style,
23
+ }: KeyboardAwareFormWrapperProps) {
24
+ return (
25
+ <KeyboardAvoidingView
26
+ style={[styles.flex, style]}
27
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
28
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
29
+ >
30
+ <ScrollView
31
+ ref={keyboardManager.scrollViewRef}
32
+ keyboardShouldPersistTaps="handled"
33
+ contentContainerStyle={[styles.content, contentContainerStyle]}
34
+ showsVerticalScrollIndicator={false}
35
+ >
36
+ {children}
37
+ </ScrollView>
38
+ </KeyboardAvoidingView>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ flex: { flex: 1 },
44
+ content: { flexGrow: 1 },
45
+ });
@@ -0,0 +1,2 @@
1
+ export { KeyboardAwareFormWrapper } from './KeyboardAwareFormWrapper';
2
+ export type { KeyboardAwareFormWrapperProps } from './KeyboardAwareFormWrapper';
@@ -0,0 +1,12 @@
1
+ export { useKeyboardManager } from './useKeyboardManager';
2
+ export type { UseKeyboardManagerOptions, UseKeyboardManagerResult, KeyboardManagerField } from './useKeyboardManager';
3
+
4
+ export { useFieldFlowForm } from './useFieldFlowForm';
5
+ export type {
6
+ UseFieldFlowFormOptions,
7
+ UseFieldFlowFormResult,
8
+ FieldFlowFormRules,
9
+ } from './useFieldFlowForm';
10
+
11
+ export { useFieldState } from './useFieldState';
12
+ export type { UseFieldStateOptions } from './useFieldState';
@@ -0,0 +1,133 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type { FieldFlowRules } from '../types';
3
+ import { runRules } from '../validation/validators';
4
+
5
+ export type FieldFlowFormRules<T extends Record<string, string>> = {
6
+ [K in keyof T]?: FieldFlowRules;
7
+ };
8
+
9
+ export interface UseFieldFlowFormOptions<T extends Record<string, string>> {
10
+ initialValues: T;
11
+ rules?: FieldFlowFormRules<T>;
12
+ /** Called only when every field passes validation. */
13
+ onSubmit?: (values: T) => void | Promise<void>;
14
+ }
15
+
16
+ export interface UseFieldFlowFormResult<T extends Record<string, string>> {
17
+ values: T;
18
+ errors: Partial<Record<keyof T, string>>;
19
+ touched: Partial<Record<keyof T, boolean>>;
20
+ isSubmitting: boolean;
21
+ isValid: boolean;
22
+ setValue: (field: keyof T, value: string) => void;
23
+ handleChange: (field: keyof T) => (value: string) => void;
24
+ handleBlur: (field: keyof T) => () => void;
25
+ validateField: (field: keyof T) => boolean;
26
+ validateAll: () => boolean;
27
+ submit: () => Promise<void>;
28
+ reset: () => void;
29
+ }
30
+
31
+ /**
32
+ * Drop-in form state manager for a set of FieldFlow inputs. Handles values,
33
+ * per-field + whole-form validation (via the same `FieldFlowRules` used by
34
+ * the input components), touched-state, and submission.
35
+ */
36
+ export function useFieldFlowForm<T extends Record<string, string>>({
37
+ initialValues,
38
+ rules,
39
+ onSubmit,
40
+ }: UseFieldFlowFormOptions<T>): UseFieldFlowFormResult<T> {
41
+ const [values, setValues] = useState<T>(initialValues);
42
+ const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
43
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
44
+ const [isSubmitting, setIsSubmitting] = useState(false);
45
+
46
+ const validateField = useCallback(
47
+ (field: keyof T): boolean => {
48
+ const fieldRules = rules?.[field];
49
+ const result = runRules(values[field] ?? '', fieldRules);
50
+ setErrors(prev => ({ ...prev, [field]: result.valid ? undefined : result.message }));
51
+ return result.valid;
52
+ },
53
+ [rules, values],
54
+ );
55
+
56
+ const validateAll = useCallback((): boolean => {
57
+ if (!rules) return true;
58
+ let allValid = true;
59
+ const nextErrors: Partial<Record<keyof T, string>> = {};
60
+ for (const key of Object.keys(rules) as (keyof T)[]) {
61
+ const result = runRules(values[key] ?? '', rules[key]);
62
+ if (!result.valid) {
63
+ allValid = false;
64
+ nextErrors[key] = result.message;
65
+ }
66
+ }
67
+ setErrors(nextErrors);
68
+ setTouched(prev => {
69
+ const next = { ...prev };
70
+ for (const key of Object.keys(rules)) next[key as keyof T] = true;
71
+ return next;
72
+ });
73
+ return allValid;
74
+ }, [rules, values]);
75
+
76
+ const setValue = useCallback((field: keyof T, value: string) => {
77
+ setValues(prev => ({ ...prev, [field]: value }));
78
+ }, []);
79
+
80
+ const handleChange = useCallback(
81
+ (field: keyof T) => (value: string) => {
82
+ setValue(field, value);
83
+ if (touched[field]) {
84
+ // live-validate once the user has already blurred this field once
85
+ const result = runRules(value, rules?.[field]);
86
+ setErrors(prev => ({ ...prev, [field]: result.valid ? undefined : result.message }));
87
+ }
88
+ },
89
+ [rules, setValue, touched],
90
+ );
91
+
92
+ const handleBlur = useCallback(
93
+ (field: keyof T) => () => {
94
+ setTouched(prev => ({ ...prev, [field]: true }));
95
+ validateField(field);
96
+ },
97
+ [validateField],
98
+ );
99
+
100
+ const submit = useCallback(async () => {
101
+ const valid = validateAll();
102
+ if (!valid) return;
103
+ setIsSubmitting(true);
104
+ try {
105
+ await onSubmit?.(values);
106
+ } finally {
107
+ setIsSubmitting(false);
108
+ }
109
+ }, [onSubmit, validateAll, values]);
110
+
111
+ const reset = useCallback(() => {
112
+ setValues(initialValues);
113
+ setErrors({});
114
+ setTouched({});
115
+ }, [initialValues]);
116
+
117
+ const isValid = useMemo(() => Object.values(errors).every(e => !e), [errors]);
118
+
119
+ return {
120
+ values,
121
+ errors,
122
+ touched,
123
+ isSubmitting,
124
+ isValid,
125
+ setValue,
126
+ handleChange,
127
+ handleBlur,
128
+ validateField,
129
+ validateAll,
130
+ submit,
131
+ reset,
132
+ };
133
+ }
@@ -0,0 +1,31 @@
1
+ import { useMemo } from 'react';
2
+ import type { FieldFlowState } from '../types';
3
+
4
+ export interface UseFieldStateOptions {
5
+ isFocused: boolean;
6
+ hasError: boolean;
7
+ isSuccess?: boolean;
8
+ disabled?: boolean;
9
+ readOnly?: boolean;
10
+ loading?: boolean;
11
+ }
12
+
13
+ /** Resolves the many boolean flags a field can have into one canonical visual state. */
14
+ export function useFieldState({
15
+ isFocused,
16
+ hasError,
17
+ isSuccess,
18
+ disabled,
19
+ readOnly,
20
+ loading,
21
+ }: UseFieldStateOptions): FieldFlowState {
22
+ return useMemo<FieldFlowState>(() => {
23
+ if (disabled) return 'disabled';
24
+ if (readOnly) return 'readonly';
25
+ if (loading) return 'loading';
26
+ if (hasError) return 'error';
27
+ if (isSuccess) return 'success';
28
+ if (isFocused) return 'focused';
29
+ return 'idle';
30
+ }, [disabled, readOnly, loading, hasError, isSuccess, isFocused]);
31
+ }
@@ -0,0 +1,105 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import type { ScrollView, TextInput } from 'react-native';
3
+
4
+ export interface KeyboardManagerField {
5
+ ref: React.RefObject<TextInput | null>;
6
+ /** y-position of this field within the ScrollView, used for auto-scroll. Optional. */
7
+ offsetY?: number;
8
+ }
9
+
10
+ export interface UseKeyboardManagerResult {
11
+ /** Attach to a field's `ref` and pass the returned `onSubmitEditing`/`returnKeyType` down. */
12
+ registerField: (id: string, ref: React.RefObject<TextInput | null>, offsetY?: number) => void;
13
+ unregisterField: (id: string) => void;
14
+ /** Call from a field's onSubmitEditing to move focus to the next registered field, or submit if it's last. */
15
+ focusNext: (id: string) => void;
16
+ focusField: (id: string) => void;
17
+ /** Attach to the surrounding ScrollView so FieldFlow can scroll the active field into view. */
18
+ scrollViewRef: React.RefObject<ScrollView | null>;
19
+ /** returnKeyType to use for a field given its position ("next" for all but the last, "done" for the last). */
20
+ getReturnKeyType: (id: string) => 'next' | 'done';
21
+ onSubmit: () => void;
22
+ }
23
+
24
+ export interface UseKeyboardManagerOptions {
25
+ /** Ordered list of field ids, e.g. ['name', 'email', 'password']. Determines Next/Done order. */
26
+ order: string[];
27
+ /** Called when "Done" is pressed on the last field, or focusNext is called past the end. */
28
+ onSubmit?: () => void;
29
+ /** Extra offset (px) added above the target field when auto-scrolling, e.g. for a sticky header. */
30
+ scrollOffset?: number;
31
+ }
32
+
33
+ /**
34
+ * Coordinates focus across many inputs in a form: pressing "Next" on the keyboard
35
+ * moves to the next field automatically, "Done" on the last field fires onSubmit,
36
+ * and the surrounding ScrollView auto-scrolls so the focused field stays visible
37
+ * above the keyboard.
38
+ */
39
+ export function useKeyboardManager({
40
+ order,
41
+ onSubmit,
42
+ scrollOffset = 24,
43
+ }: UseKeyboardManagerOptions): UseKeyboardManagerResult {
44
+ const fields = useRef<Map<string, KeyboardManagerField>>(new Map());
45
+ const scrollViewRef = useRef<ScrollView>(null);
46
+
47
+ const registerField = useCallback((id: string, ref: React.RefObject<TextInput | null>, offsetY?: number) => {
48
+ fields.current.set(id, { ref, offsetY });
49
+ }, []);
50
+
51
+ const unregisterField = useCallback((id: string) => {
52
+ fields.current.delete(id);
53
+ }, []);
54
+
55
+ const scrollToField = useCallback(
56
+ (id: string) => {
57
+ const field = fields.current.get(id);
58
+ if (field?.offsetY !== undefined && scrollViewRef.current) {
59
+ scrollViewRef.current.scrollTo({ y: Math.max(0, field.offsetY - scrollOffset), animated: true });
60
+ }
61
+ },
62
+ [scrollOffset],
63
+ );
64
+
65
+ const focusField = useCallback(
66
+ (id: string) => {
67
+ const field = fields.current.get(id);
68
+ field?.ref.current?.focus();
69
+ scrollToField(id);
70
+ },
71
+ [scrollToField],
72
+ );
73
+
74
+ const focusNext = useCallback(
75
+ (id: string) => {
76
+ const currentIndex = order.indexOf(id);
77
+ const nextId = order[currentIndex + 1];
78
+ if (nextId) {
79
+ focusField(nextId);
80
+ } else {
81
+ fields.current.get(id)?.ref.current?.blur();
82
+ onSubmit?.();
83
+ }
84
+ },
85
+ [order, focusField, onSubmit],
86
+ );
87
+
88
+ const getReturnKeyType = useCallback(
89
+ (id: string): 'next' | 'done' => {
90
+ const isLast = order.indexOf(id) === order.length - 1;
91
+ return isLast ? 'done' : 'next';
92
+ },
93
+ [order],
94
+ );
95
+
96
+ return {
97
+ registerField,
98
+ unregisterField,
99
+ focusNext,
100
+ focusField,
101
+ scrollViewRef,
102
+ getReturnKeyType,
103
+ onSubmit: () => onSubmit?.(),
104
+ };
105
+ }
@@ -0,0 +1,338 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, View, ViewStyle } from 'react-native';
3
+
4
+ export type FieldFlowIconName =
5
+ | 'eye'
6
+ | 'eyeOff'
7
+ | 'clear'
8
+ | 'check'
9
+ | 'alert'
10
+ | 'search'
11
+ | 'send'
12
+ | 'camera'
13
+ | 'image'
14
+ | 'paperclip'
15
+ | 'mic'
16
+ | 'mapPin'
17
+ | 'spinner';
18
+
19
+ export interface FieldFlowIconProps {
20
+ name: FieldFlowIconName;
21
+ size?: number;
22
+ color?: string;
23
+ style?: ViewStyle;
24
+ }
25
+
26
+ /**
27
+ * A minimal, dependency-free icon set built out of basic Views. Ships so consumers
28
+ * don't need to install react-native-svg / vector-icons just to use FieldFlow.
29
+ * Every icon accepts `size` and `color`, and you can always render your own
30
+ * icon via the `leftIcon` / `rightIcon` props on any FieldFlow component instead.
31
+ */
32
+ export function FieldFlowIcon({ name, size = 18, color = '#6B7280', style }: FieldFlowIconProps) {
33
+ if (name === 'spinner') {
34
+ return <SpinnerIcon size={size} color={color} style={style} />;
35
+ }
36
+
37
+ const stroke = Math.max(1.5, size * 0.09);
38
+ const box: ViewStyle = { width: size, height: size, alignItems: 'center', justifyContent: 'center' };
39
+
40
+ switch (name) {
41
+ case 'eye':
42
+ return (
43
+ <View style={[box, style]}>
44
+ <View
45
+ style={{
46
+ width: size,
47
+ height: size * 0.55,
48
+ borderRadius: size * 0.55,
49
+ borderWidth: stroke,
50
+ borderColor: color,
51
+ }}
52
+ />
53
+ <View
54
+ style={{
55
+ position: 'absolute',
56
+ width: size * 0.3,
57
+ height: size * 0.3,
58
+ borderRadius: size * 0.15,
59
+ backgroundColor: color,
60
+ }}
61
+ />
62
+ </View>
63
+ );
64
+ case 'eyeOff':
65
+ return (
66
+ <View style={[box, style]}>
67
+ <View
68
+ style={{
69
+ width: size,
70
+ height: size * 0.55,
71
+ borderRadius: size * 0.55,
72
+ borderWidth: stroke,
73
+ borderColor: color,
74
+ opacity: 0.6,
75
+ }}
76
+ />
77
+ <View
78
+ style={{
79
+ position: 'absolute',
80
+ width: size * 1.15,
81
+ height: stroke,
82
+ backgroundColor: color,
83
+ transform: [{ rotate: '45deg' }],
84
+ }}
85
+ />
86
+ </View>
87
+ );
88
+ case 'clear':
89
+ return (
90
+ <View style={[box, style]}>
91
+ <View
92
+ style={{
93
+ width: size,
94
+ height: size,
95
+ borderRadius: size / 2,
96
+ backgroundColor: color,
97
+ opacity: 0.15,
98
+ position: 'absolute',
99
+ }}
100
+ />
101
+ <View style={{ width: size * 0.5, height: stroke, backgroundColor: color, transform: [{ rotate: '45deg' }], position: 'absolute' }} />
102
+ <View style={{ width: size * 0.5, height: stroke, backgroundColor: color, transform: [{ rotate: '-45deg' }], position: 'absolute' }} />
103
+ </View>
104
+ );
105
+ case 'check':
106
+ return (
107
+ <View style={[box, style]}>
108
+ <View
109
+ style={{
110
+ width: size * 0.55,
111
+ height: stroke,
112
+ backgroundColor: color,
113
+ transform: [{ rotate: '45deg' }, { translateY: size * 0.12 }, { translateX: -size * 0.08 }],
114
+ position: 'absolute',
115
+ }}
116
+ />
117
+ <View
118
+ style={{
119
+ width: size * 0.3,
120
+ height: stroke,
121
+ backgroundColor: color,
122
+ transform: [{ rotate: '-45deg' }, { translateY: size * 0.05 }, { translateX: -size * 0.22 }],
123
+ position: 'absolute',
124
+ }}
125
+ />
126
+ </View>
127
+ );
128
+ case 'alert':
129
+ return (
130
+ <View style={[box, style]}>
131
+ <View
132
+ style={{
133
+ width: size,
134
+ height: size,
135
+ borderRadius: size / 2,
136
+ borderWidth: stroke,
137
+ borderColor: color,
138
+ alignItems: 'center',
139
+ justifyContent: 'center',
140
+ }}
141
+ >
142
+ <View style={{ width: stroke, height: size * 0.32, backgroundColor: color, borderRadius: stroke, marginBottom: size * 0.08 }} />
143
+ <View style={{ width: stroke, height: stroke, borderRadius: stroke / 2, backgroundColor: color }} />
144
+ </View>
145
+ </View>
146
+ );
147
+ case 'search':
148
+ return (
149
+ <View style={[box, style]}>
150
+ <View
151
+ style={{
152
+ width: size * 0.7,
153
+ height: size * 0.7,
154
+ borderRadius: size * 0.35,
155
+ borderWidth: stroke,
156
+ borderColor: color,
157
+ position: 'absolute',
158
+ top: 0,
159
+ left: 0,
160
+ }}
161
+ />
162
+ <View
163
+ style={{
164
+ width: size * 0.4,
165
+ height: stroke,
166
+ backgroundColor: color,
167
+ position: 'absolute',
168
+ bottom: 0,
169
+ right: 0,
170
+ borderRadius: stroke,
171
+ transform: [{ rotate: '45deg' }],
172
+ }}
173
+ />
174
+ </View>
175
+ );
176
+ case 'send':
177
+ return (
178
+ <View style={[box, style]}>
179
+ <View
180
+ style={{
181
+ width: 0,
182
+ height: 0,
183
+ borderTopWidth: size * 0.5,
184
+ borderBottomWidth: size * 0.5,
185
+ borderLeftWidth: size * 0.7,
186
+ borderTopColor: 'transparent',
187
+ borderBottomColor: 'transparent',
188
+ borderLeftColor: color,
189
+ transform: [{ rotate: '0deg' }],
190
+ }}
191
+ />
192
+ </View>
193
+ );
194
+ case 'camera':
195
+ return (
196
+ <View style={[box, style]}>
197
+ <View
198
+ style={{
199
+ width: size,
200
+ height: size * 0.75,
201
+ borderRadius: size * 0.15,
202
+ borderWidth: stroke,
203
+ borderColor: color,
204
+ alignItems: 'center',
205
+ justifyContent: 'center',
206
+ }}
207
+ >
208
+ <View style={{ width: size * 0.36, height: size * 0.36, borderRadius: size * 0.18, borderWidth: stroke, borderColor: color }} />
209
+ </View>
210
+ </View>
211
+ );
212
+ case 'image':
213
+ return (
214
+ <View style={[box, style]}>
215
+ <View
216
+ style={{
217
+ width: size,
218
+ height: size * 0.8,
219
+ borderRadius: size * 0.12,
220
+ borderWidth: stroke,
221
+ borderColor: color,
222
+ overflow: 'hidden',
223
+ }}
224
+ >
225
+ <View
226
+ style={{
227
+ position: 'absolute',
228
+ bottom: 0,
229
+ left: size * 0.1,
230
+ width: size * 0.3,
231
+ height: size * 0.35,
232
+ backgroundColor: color,
233
+ opacity: 0.8,
234
+ }}
235
+ />
236
+ <View
237
+ style={{
238
+ position: 'absolute',
239
+ top: size * 0.12,
240
+ right: size * 0.15,
241
+ width: size * 0.16,
242
+ height: size * 0.16,
243
+ borderRadius: size * 0.08,
244
+ backgroundColor: color,
245
+ }}
246
+ />
247
+ </View>
248
+ </View>
249
+ );
250
+ case 'paperclip':
251
+ return (
252
+ <View style={[box, style]}>
253
+ <View
254
+ style={{
255
+ width: size * 0.36,
256
+ height: size * 0.9,
257
+ borderRadius: size * 0.18,
258
+ borderWidth: stroke,
259
+ borderColor: color,
260
+ transform: [{ rotate: '20deg' }],
261
+ }}
262
+ />
263
+ </View>
264
+ );
265
+ case 'mic':
266
+ return (
267
+ <View style={[box, style]}>
268
+ <View
269
+ style={{
270
+ width: size * 0.4,
271
+ height: size * 0.65,
272
+ borderRadius: size * 0.2,
273
+ borderWidth: stroke,
274
+ borderColor: color,
275
+ marginBottom: size * 0.05,
276
+ }}
277
+ />
278
+ <View style={{ width: size * 0.6, height: stroke, backgroundColor: color, borderRadius: stroke }} />
279
+ </View>
280
+ );
281
+ case 'mapPin':
282
+ return (
283
+ <View style={[box, style]}>
284
+ <View
285
+ style={{
286
+ width: size * 0.7,
287
+ height: size * 0.7,
288
+ borderRadius: size * 0.35,
289
+ borderWidth: stroke,
290
+ borderColor: color,
291
+ alignItems: 'center',
292
+ justifyContent: 'center',
293
+ }}
294
+ >
295
+ <View style={{ width: size * 0.2, height: size * 0.2, borderRadius: size * 0.1, backgroundColor: color }} />
296
+ </View>
297
+ </View>
298
+ );
299
+ default:
300
+ return <View style={[box, style]} />;
301
+ }
302
+ }
303
+
304
+ function SpinnerIcon({ size, color, style }: { size: number; color: string; style?: ViewStyle }) {
305
+ const spin = useRef(new Animated.Value(0)).current;
306
+
307
+ useEffect(() => {
308
+ const loop = Animated.loop(
309
+ Animated.timing(spin, {
310
+ toValue: 1,
311
+ duration: 800,
312
+ easing: Easing.linear,
313
+ useNativeDriver: true,
314
+ }),
315
+ );
316
+ loop.start();
317
+ return () => loop.stop();
318
+ }, [spin]);
319
+
320
+ const rotate = spin.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] });
321
+
322
+ return (
323
+ <Animated.View
324
+ style={[
325
+ {
326
+ width: size,
327
+ height: size,
328
+ borderRadius: size / 2,
329
+ borderWidth: Math.max(1.5, size * 0.12),
330
+ borderColor: color,
331
+ borderTopColor: 'transparent',
332
+ transform: [{ rotate }],
333
+ },
334
+ style,
335
+ ]}
336
+ />
337
+ );
338
+ }
@@ -0,0 +1,2 @@
1
+ export { FieldFlowIcon } from './Icon';
2
+ export type { FieldFlowIconName, FieldFlowIconProps } from './Icon';