@oxyhq/services 5.7.5 → 5.8.2

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 (303) hide show
  1. package/README.md +76 -76
  2. package/lib/commonjs/core/index.js +177 -102
  3. package/lib/commonjs/core/index.js.map +1 -1
  4. package/lib/commonjs/index.js +69 -28
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/ui/components/Avatar.js +15 -6
  7. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  8. package/lib/commonjs/ui/components/FollowButton.js +100 -12
  9. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  10. package/lib/commonjs/ui/components/GroupedItem.js +58 -13
  11. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  12. package/lib/commonjs/ui/components/GroupedSection.js +7 -1
  13. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  14. package/lib/commonjs/ui/components/Header.js +356 -0
  15. package/lib/commonjs/ui/components/Header.js.map +1 -0
  16. package/lib/commonjs/ui/components/OxyProvider.js +28 -7
  17. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  18. package/lib/commonjs/ui/components/index.js +7 -0
  19. package/lib/commonjs/ui/components/index.js.map +1 -1
  20. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +1 -1
  21. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  22. package/lib/commonjs/ui/components/internal/TextField.js +606 -546
  23. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  24. package/lib/commonjs/ui/components/internal/TextField.md +436 -0
  25. package/lib/commonjs/ui/context/OxyContext.js +181 -199
  26. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/index.js +6 -0
  28. package/lib/commonjs/ui/hooks/index.js.map +1 -1
  29. package/lib/commonjs/ui/hooks/useFollow.js +59 -2
  30. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  31. package/lib/commonjs/ui/hooks/useSessionSocket.js +5 -2
  32. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  33. package/lib/commonjs/ui/navigation/OxyRouter.js +11 -1
  34. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  35. package/lib/commonjs/ui/screens/AccountCenterScreen.js +6 -6
  36. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/AccountManagementDemo.js +3 -3
  38. package/lib/commonjs/ui/screens/AccountManagementDemo.js.map +1 -1
  39. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +241 -598
  40. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  41. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +1160 -406
  42. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  43. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +135 -237
  44. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  45. package/lib/commonjs/ui/screens/AppInfoScreen.js +246 -463
  46. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  47. package/lib/commonjs/ui/screens/FeedbackScreen.js +3 -3
  48. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
  49. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +808 -650
  50. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
  51. package/lib/commonjs/ui/screens/ProfileScreen.js +214 -37
  52. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  53. package/lib/commonjs/ui/screens/RecoverAccountScreen.js +51 -72
  54. package/lib/commonjs/ui/screens/RecoverAccountScreen.js.map +1 -1
  55. package/lib/commonjs/ui/screens/SessionManagementScreen.js +11 -29
  56. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  57. package/lib/commonjs/ui/screens/SignInScreen.js +30 -303
  58. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  59. package/lib/commonjs/ui/screens/SignUpScreen.js +4 -4
  60. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  61. package/lib/commonjs/ui/screens/UserLinksScreen.js +90 -0
  62. package/lib/commonjs/ui/screens/UserLinksScreen.js.map +1 -0
  63. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +19 -31
  64. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  65. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +7 -10
  66. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  67. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js +11 -5
  68. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js.map +1 -1
  69. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js +11 -4
  70. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js.map +1 -1
  71. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js +9 -6
  72. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  73. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +3 -30
  74. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  75. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js +37 -46
  76. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
  77. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js +9 -12
  78. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  79. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +9 -12
  80. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  81. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js +9 -12
  82. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  83. package/lib/commonjs/ui/stores/authStore.js +36 -6
  84. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  85. package/lib/commonjs/ui/stores/followStore.js +106 -1
  86. package/lib/commonjs/ui/stores/followStore.js.map +1 -1
  87. package/lib/commonjs/ui/styles/authStyles.js +337 -0
  88. package/lib/commonjs/ui/styles/authStyles.js.map +1 -0
  89. package/lib/commonjs/ui/styles/index.js +11 -0
  90. package/lib/commonjs/ui/styles/index.js.map +1 -1
  91. package/lib/module/core/index.js +177 -41
  92. package/lib/module/core/index.js.map +1 -1
  93. package/lib/module/index.js +24 -4
  94. package/lib/module/index.js.map +1 -1
  95. package/lib/module/ui/components/Avatar.js +15 -6
  96. package/lib/module/ui/components/Avatar.js.map +1 -1
  97. package/lib/module/ui/components/FollowButton.js +101 -13
  98. package/lib/module/ui/components/FollowButton.js.map +1 -1
  99. package/lib/module/ui/components/GroupedItem.js +59 -14
  100. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  101. package/lib/module/ui/components/GroupedSection.js +7 -1
  102. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  103. package/lib/module/ui/components/Header.js +351 -0
  104. package/lib/module/ui/components/Header.js.map +1 -0
  105. package/lib/module/ui/components/OxyProvider.js +30 -9
  106. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  107. package/lib/module/ui/components/index.js +1 -0
  108. package/lib/module/ui/components/index.js.map +1 -1
  109. package/lib/module/ui/components/internal/GroupedPillButtons.js +1 -1
  110. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  111. package/lib/module/ui/components/internal/TextField.js +607 -547
  112. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  113. package/lib/module/ui/components/internal/TextField.md +436 -0
  114. package/lib/module/ui/context/OxyContext.js +180 -198
  115. package/lib/module/ui/context/OxyContext.js.map +1 -1
  116. package/lib/module/ui/hooks/index.js +1 -1
  117. package/lib/module/ui/hooks/index.js.map +1 -1
  118. package/lib/module/ui/hooks/useFollow.js +57 -1
  119. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  120. package/lib/module/ui/hooks/useSessionSocket.js +5 -2
  121. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  122. package/lib/module/ui/navigation/OxyRouter.js +11 -1
  123. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  124. package/lib/module/ui/screens/AccountCenterScreen.js +6 -6
  125. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  126. package/lib/module/ui/screens/AccountManagementDemo.js +3 -3
  127. package/lib/module/ui/screens/AccountManagementDemo.js.map +1 -1
  128. package/lib/module/ui/screens/AccountOverviewScreen.js +242 -597
  129. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  130. package/lib/module/ui/screens/AccountSettingsScreen.js +1161 -407
  131. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  132. package/lib/module/ui/screens/AccountSwitcherScreen.js +135 -237
  133. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  134. package/lib/module/ui/screens/AppInfoScreen.js +248 -465
  135. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  136. package/lib/module/ui/screens/FeedbackScreen.js +3 -3
  137. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  138. package/lib/module/ui/screens/PaymentGatewayScreen.js +809 -651
  139. package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
  140. package/lib/module/ui/screens/ProfileScreen.js +214 -37
  141. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  142. package/lib/module/ui/screens/RecoverAccountScreen.js +53 -74
  143. package/lib/module/ui/screens/RecoverAccountScreen.js.map +1 -1
  144. package/lib/module/ui/screens/SessionManagementScreen.js +11 -29
  145. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  146. package/lib/module/ui/screens/SignInScreen.js +32 -305
  147. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  148. package/lib/module/ui/screens/SignUpScreen.js +5 -5
  149. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  150. package/lib/module/ui/screens/UserLinksScreen.js +85 -0
  151. package/lib/module/ui/screens/UserLinksScreen.js.map +1 -0
  152. package/lib/module/ui/screens/internal/SignInPasswordStep.js +19 -31
  153. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  154. package/lib/module/ui/screens/internal/SignInUsernameStep.js +7 -10
  155. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  156. package/lib/module/ui/screens/internal/SignUpIdentityStep.js +11 -5
  157. package/lib/module/ui/screens/internal/SignUpIdentityStep.js.map +1 -1
  158. package/lib/module/ui/screens/internal/SignUpSecurityStep.js +11 -4
  159. package/lib/module/ui/screens/internal/SignUpSecurityStep.js.map +1 -1
  160. package/lib/module/ui/screens/karma/KarmaAboutScreen.js +9 -6
  161. package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  162. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +3 -30
  163. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  164. package/lib/module/ui/screens/karma/KarmaFAQScreen.js +37 -46
  165. package/lib/module/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
  166. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js +9 -12
  167. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  168. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +9 -12
  169. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  170. package/lib/module/ui/screens/karma/KarmaRulesScreen.js +9 -12
  171. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  172. package/lib/module/ui/stores/authStore.js +36 -6
  173. package/lib/module/ui/stores/authStore.js.map +1 -1
  174. package/lib/module/ui/stores/followStore.js +106 -1
  175. package/lib/module/ui/stores/followStore.js.map +1 -1
  176. package/lib/module/ui/styles/authStyles.js +332 -0
  177. package/lib/module/ui/styles/authStyles.js.map +1 -0
  178. package/lib/module/ui/styles/index.js +1 -0
  179. package/lib/module/ui/styles/index.js.map +1 -1
  180. package/lib/typescript/core/index.d.ts +68 -24
  181. package/lib/typescript/core/index.d.ts.map +1 -1
  182. package/lib/typescript/index.d.ts +13 -3
  183. package/lib/typescript/index.d.ts.map +1 -1
  184. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  185. package/lib/typescript/ui/components/FollowButton.d.ts +1 -0
  186. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  187. package/lib/typescript/ui/components/GroupedItem.d.ts +6 -0
  188. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  189. package/lib/typescript/ui/components/GroupedSection.d.ts +6 -0
  190. package/lib/typescript/ui/components/GroupedSection.d.ts.map +1 -1
  191. package/lib/typescript/ui/components/Header.d.ts +24 -0
  192. package/lib/typescript/ui/components/Header.d.ts.map +1 -0
  193. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  194. package/lib/typescript/ui/components/index.d.ts +1 -0
  195. package/lib/typescript/ui/components/index.d.ts.map +1 -1
  196. package/lib/typescript/ui/components/internal/TextField.d.ts +31 -16
  197. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  198. package/lib/typescript/ui/context/OxyContext.d.ts +5 -2
  199. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  200. package/lib/typescript/ui/hooks/index.d.ts +1 -1
  201. package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
  202. package/lib/typescript/ui/hooks/useFollow.d.ts +20 -0
  203. package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -1
  204. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  205. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  206. package/lib/typescript/ui/navigation/types.d.ts +9 -2
  207. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  208. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  209. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  210. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  211. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  212. package/lib/typescript/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  213. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  214. package/lib/typescript/ui/screens/RecoverAccountScreen.d.ts +5 -1
  215. package/lib/typescript/ui/screens/RecoverAccountScreen.d.ts.map +1 -1
  216. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  217. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  218. package/lib/typescript/ui/screens/UserLinksScreen.d.ts +15 -0
  219. package/lib/typescript/ui/screens/UserLinksScreen.d.ts.map +1 -0
  220. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts +1 -1
  221. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  222. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts +0 -1
  223. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts.map +1 -1
  224. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts.map +1 -1
  225. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts.map +1 -1
  226. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts.map +1 -1
  227. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts.map +1 -1
  228. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts.map +1 -1
  229. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +1 -1
  230. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -1
  231. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts.map +1 -1
  232. package/lib/typescript/ui/stores/authStore.d.ts +3 -1
  233. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  234. package/lib/typescript/ui/stores/followStore.d.ts +10 -0
  235. package/lib/typescript/ui/stores/followStore.d.ts.map +1 -1
  236. package/lib/typescript/ui/styles/authStyles.d.ts +326 -0
  237. package/lib/typescript/ui/styles/authStyles.d.ts.map +1 -0
  238. package/lib/typescript/ui/styles/index.d.ts +1 -0
  239. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  240. package/package.json +1 -4
  241. package/src/core/index.ts +195 -41
  242. package/src/index.ts +64 -4
  243. package/src/ui/components/Avatar.tsx +11 -5
  244. package/src/ui/components/FollowButton.tsx +95 -11
  245. package/src/ui/components/GroupedItem.tsx +57 -9
  246. package/src/ui/components/GroupedSection.tsx +12 -0
  247. package/src/ui/components/Header.tsx +405 -0
  248. package/src/ui/components/OxyProvider.tsx +37 -15
  249. package/src/ui/components/index.ts +1 -0
  250. package/src/ui/components/internal/GroupedPillButtons.tsx +1 -1
  251. package/src/ui/components/internal/TextField.md +436 -0
  252. package/src/ui/components/internal/TextField.tsx +720 -620
  253. package/src/ui/context/OxyContext.tsx +211 -195
  254. package/src/ui/hooks/index.ts +1 -1
  255. package/src/ui/hooks/useFollow.ts +63 -0
  256. package/src/ui/hooks/useSessionSocket.ts +5 -2
  257. package/src/ui/navigation/OxyRouter.tsx +11 -1
  258. package/src/ui/navigation/types.ts +10 -2
  259. package/src/ui/screens/AccountCenterScreen.tsx +5 -5
  260. package/src/ui/screens/AccountManagementDemo.tsx +9 -9
  261. package/src/ui/screens/AccountOverviewScreen.tsx +265 -414
  262. package/src/ui/screens/AccountSettingsScreen.tsx +1173 -403
  263. package/src/ui/screens/AccountSwitcherScreen.tsx +158 -202
  264. package/src/ui/screens/AppInfoScreen.tsx +270 -497
  265. package/src/ui/screens/FeedbackScreen.tsx +3 -3
  266. package/src/ui/screens/PaymentGatewayScreen.tsx +668 -365
  267. package/src/ui/screens/ProfileScreen.tsx +196 -33
  268. package/src/ui/screens/RecoverAccountScreen.tsx +46 -74
  269. package/src/ui/screens/SessionManagementScreen.tsx +14 -22
  270. package/src/ui/screens/SignInScreen.tsx +27 -294
  271. package/src/ui/screens/SignUpScreen.tsx +5 -5
  272. package/src/ui/screens/UserLinksScreen.tsx +96 -0
  273. package/src/ui/screens/internal/SignInPasswordStep.tsx +11 -22
  274. package/src/ui/screens/internal/SignInUsernameStep.tsx +3 -10
  275. package/src/ui/screens/internal/SignUpIdentityStep.tsx +2 -5
  276. package/src/ui/screens/internal/SignUpSecurityStep.tsx +3 -4
  277. package/src/ui/screens/karma/KarmaAboutScreen.tsx +9 -2
  278. package/src/ui/screens/karma/KarmaCenterScreen.tsx +1 -20
  279. package/src/ui/screens/karma/KarmaFAQScreen.tsx +40 -24
  280. package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +9 -3
  281. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +9 -3
  282. package/src/ui/screens/karma/KarmaRulesScreen.tsx +9 -3
  283. package/src/ui/stores/authStore.ts +34 -7
  284. package/src/ui/stores/followStore.ts +102 -1
  285. package/src/ui/styles/authStyles.ts +352 -0
  286. package/src/ui/styles/index.ts +1 -0
  287. package/lib/commonjs/core/auth-manager.js +0 -440
  288. package/lib/commonjs/core/auth-manager.js.map +0 -1
  289. package/lib/commonjs/core/use-auth.js +0 -244
  290. package/lib/commonjs/core/use-auth.js.map +0 -1
  291. package/lib/module/core/auth-manager.js +0 -432
  292. package/lib/module/core/auth-manager.js.map +0 -1
  293. package/lib/module/core/use-auth.js +0 -235
  294. package/lib/module/core/use-auth.js.map +0 -1
  295. package/lib/typescript/core/auth-manager.d.ts +0 -136
  296. package/lib/typescript/core/auth-manager.d.ts.map +0 -1
  297. package/lib/typescript/core/use-auth.d.ts +0 -79
  298. package/lib/typescript/core/use-auth.d.ts.map +0 -1
  299. package/src/__tests__/middleware.test.ts +0 -105
  300. package/src/__tests__/setup.ts +0 -10
  301. package/src/__tests__/zero-config-auth.test.ts +0 -607
  302. package/src/core/auth-manager.ts +0 -500
  303. package/src/core/use-auth.tsx +0 -245
@@ -10,694 +10,794 @@ import {
10
10
  TextInputProps,
11
11
  Animated,
12
12
  LayoutChangeEvent,
13
+ AccessibilityInfo,
14
+ StyleProp,
15
+ ViewStyle,
16
+ NativeSyntheticEvent,
17
+ TargetedEvent,
18
+ TextInputFocusEventData,
13
19
  } from 'react-native';
14
20
  import { Ionicons } from '@expo/vector-icons';
15
21
  import Svg, { Path } from 'react-native-svg';
16
- import { Animated as RNAnimated } from 'react-native';
22
+ import { fontFamilies } from '../../styles/fonts';
17
23
 
18
24
  export interface TextFieldProps extends Omit<TextInputProps, 'style'> {
25
+ // Basic props
19
26
  label?: string;
20
- icon?: string;
21
- iconColor?: string;
27
+ variant?: 'filled' | 'outlined' | 'standard';
28
+ color?: 'primary' | 'secondary' | 'error' | 'success' | 'warning';
29
+
30
+ // Leading and trailing elements
31
+ leading?: React.ReactNode | ((props: { color: string; size: number }) => React.ReactNode | null) | null;
32
+ trailing?: React.ReactNode | ((props: { color: string; size: number }) => React.ReactNode | null) | null;
33
+
34
+ // States
22
35
  error?: string;
23
36
  success?: boolean;
24
37
  loading?: boolean;
25
- rightComponent?: React.ReactNode;
26
- leftComponent?: React.ReactNode;
27
- colors?: any;
28
- containerStyle?: any;
29
- inputStyle?: any;
30
- labelStyle?: any;
31
- errorStyle?: any;
32
- variant?: 'outlined' | 'filled';
33
- onFocus?: () => void;
34
- onBlur?: () => void;
35
- onChangeText?: (text: string) => void;
36
- testID?: string;
37
- validMessage?: string;
38
+ disabled?: boolean;
39
+
40
+ // Helper text
41
+ helperText?: string;
42
+
43
+ // Enhanced features
44
+ maxLength?: number;
45
+ showCharacterCount?: boolean;
46
+ inputMask?: 'phone' | 'creditCard' | 'currency' | 'custom';
47
+ customMask?: (value: string) => string;
48
+ formatValue?: (value: string) => string;
49
+ validateOnChange?: boolean;
50
+ debounceMs?: number;
51
+ passwordStrength?: boolean;
52
+ clearable?: boolean;
53
+
54
+ // Mouse events (for web)
55
+ onMouseEnter?: (event: NativeSyntheticEvent<TargetedEvent>) => void;
56
+ onMouseLeave?: (event: NativeSyntheticEvent<TargetedEvent>) => void;
57
+
58
+ // Styling
59
+ style?: StyleProp<ViewStyle>;
60
+ inputContainerStyle?: StyleProp<ViewStyle>;
61
+ inputStyle?: TextInputProps['style'];
62
+ leadingContainerStyle?: StyleProp<ViewStyle>;
63
+ trailingContainerStyle?: StyleProp<ViewStyle>;
64
+
65
+ // Callbacks
66
+ onValidationChange?: (isValid: boolean, value: string) => void;
67
+ onClear?: () => void;
38
68
  }
39
69
 
70
+ // Color palette for different states
71
+ const colorPalette = {
72
+ primary: {
73
+ main: '#d169e5',
74
+ light: '#e8b5f0',
75
+ dark: '#a64db3',
76
+ },
77
+ secondary: {
78
+ main: '#666666',
79
+ light: '#999999',
80
+ dark: '#333333',
81
+ },
82
+ error: {
83
+ main: '#D32F2F',
84
+ light: '#ffcdd2',
85
+ dark: '#b71c1c',
86
+ },
87
+ success: {
88
+ main: '#2E7D32',
89
+ light: '#c8e6c9',
90
+ dark: '#1b5e20',
91
+ },
92
+ warning: {
93
+ main: '#FF9800',
94
+ light: '#ffe0b2',
95
+ dark: '#e65100',
96
+ },
97
+ };
98
+
99
+ // Surface scale for consistent theming
100
+ const surfaceScale = (level: number) => {
101
+ const base = 255;
102
+ const value = Math.round(base - (level * 255));
103
+ return `#${value.toString(16).padStart(2, '0').repeat(3)}`;
104
+ };
105
+
106
+ // Password strength calculation
107
+ const calculatePasswordStrength = (password: string): { score: number; feedback: string; color: string; label: string } => {
108
+ if (!password) return { score: 0, feedback: '', color: '#E0E0E0', label: '' };
109
+
110
+ let score = 0;
111
+ const feedback: string[] = [];
112
+
113
+ if (password.length >= 8) score += 25;
114
+ else feedback.push('At least 8 characters');
115
+
116
+ if (/[A-Z]/.test(password)) score += 25;
117
+ else feedback.push('One uppercase letter');
118
+
119
+ if (/[a-z]/.test(password)) score += 25;
120
+ else feedback.push('One lowercase letter');
121
+
122
+ if (/[\d\W]/.test(password)) score += 25;
123
+ else feedback.push('One number or special character');
124
+
125
+ const colors = {
126
+ 0: '#E0E0E0',
127
+ 25: '#D32F2F',
128
+ 50: '#FF9800',
129
+ 75: '#2196F3',
130
+ 100: '#4CAF50'
131
+ };
132
+
133
+ const labels = {
134
+ 0: '',
135
+ 25: 'Weak',
136
+ 50: 'Fair',
137
+ 75: 'Good',
138
+ 100: 'Strong'
139
+ };
140
+
141
+ return {
142
+ score,
143
+ feedback: score === 100 ? 'Strong password!' : `Missing: ${feedback.join(', ')}`,
144
+ color: colors[score as keyof typeof colors] || colors[0],
145
+ label: labels[score as keyof typeof labels] || ''
146
+ };
147
+ };
148
+
149
+ // Input formatting utilities
150
+ const formatters = {
151
+ phone: (value: string) => {
152
+ const cleaned = value.replace(/\D/g, '');
153
+ const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
154
+ if (match) return `(${match[1]}) ${match[2]}-${match[3]}`;
155
+ return value;
156
+ },
157
+ creditCard: (value: string) => {
158
+ const cleaned = value.replace(/\D/g, '');
159
+ const match = cleaned.match(/^(\d{4})(\d{4})(\d{4})(\d{4})$/);
160
+ if (match) return `${match[1]} ${match[2]} ${match[3]} ${match[4]}`;
161
+ return value.replace(/(.{4})/g, '$1 ').trim();
162
+ },
163
+ currency: (value: string) => {
164
+ const cleaned = value.replace(/[^\d.]/g, '');
165
+ const num = parseFloat(cleaned);
166
+ return isNaN(num) ? value : `$${num.toFixed(2)}`;
167
+ }
168
+ };
169
+
170
+ // Debounce hook
171
+ const useDebounce = (value: string, delay: number) => {
172
+ const [debouncedValue, setDebouncedValue] = useState(value);
173
+
174
+ useEffect(() => {
175
+ const handler = setTimeout(() => {
176
+ setDebouncedValue(value);
177
+ }, delay);
178
+
179
+ return () => {
180
+ clearTimeout(handler);
181
+ };
182
+ }, [value, delay]);
183
+
184
+ return debouncedValue;
185
+ };
186
+
40
187
  const TextField = forwardRef<TextInput, TextFieldProps>(({
188
+ // Basic props
41
189
  label,
42
- icon,
43
- iconColor,
190
+ variant = 'filled',
191
+ color = 'primary',
192
+
193
+ // Leading and trailing
194
+ leading,
195
+ trailing,
196
+
197
+ // States
44
198
  error,
45
199
  success = false,
46
200
  loading = false,
47
- rightComponent,
48
- leftComponent,
49
- colors,
50
- containerStyle,
201
+ disabled = false,
202
+
203
+ // Helper text
204
+ helperText,
205
+
206
+ // Enhanced features
207
+ maxLength,
208
+ showCharacterCount,
209
+ inputMask,
210
+ customMask,
211
+ formatValue,
212
+ validateOnChange,
213
+ debounceMs = 300,
214
+ passwordStrength = false,
215
+ clearable = false,
216
+
217
+ // Mouse events
218
+ onMouseEnter,
219
+ onMouseLeave,
220
+
221
+ // Styling
222
+ style,
223
+ inputContainerStyle,
51
224
  inputStyle,
52
- labelStyle,
53
- errorStyle,
54
- variant = 'outlined',
225
+ leadingContainerStyle,
226
+ trailingContainerStyle,
227
+
228
+ // Callbacks
229
+ onValidationChange,
230
+ onClear,
231
+
232
+ // TextInput props
233
+ placeholder,
55
234
  onFocus,
56
235
  onBlur,
57
236
  onChangeText,
58
- testID,
59
- secureTextEntry,
60
237
  value = '',
61
- validMessage,
62
- ...textInputProps
238
+ secureTextEntry,
239
+ ...rest
63
240
  }, ref) => {
64
- const [isFocused, setIsFocused] = useState(false);
241
+ // State management
242
+ const [focused, setFocused] = useState(false);
243
+ const [hovered, setHovered] = useState(false);
65
244
  const [showPassword, setShowPassword] = useState(false);
66
- const [isLabelFloating, setIsLabelFloating] = useState(value ? true : false);
67
- const [labelWidth, setLabelWidth] = useState(0);
68
- const [labelLeft, setLabelLeft] = useState(0);
69
- const [inputWidth, setInputWidth] = useState(0);
70
- const [inputHeight, setInputHeight] = useState(64);
71
- const borderRadius = 16;
72
- const borderWidth = 2;
73
-
74
- // Animation values
75
- const labelAnim = useRef(new Animated.Value(value ? 1 : 0)).current;
76
- const borderAnim = useRef(new Animated.Value(0)).current;
77
-
78
- const handleFocus = useCallback(() => {
79
- setIsFocused(true);
80
- onFocus?.();
81
-
82
- // Animate label to top
83
- Animated.timing(labelAnim, {
84
- toValue: 1,
85
- duration: 200,
86
- useNativeDriver: false,
87
- }).start();
245
+ const [internalValue, setInternalValue] = useState(value);
246
+ const [isValidating, setIsValidating] = useState(false);
247
+
248
+ // Refs
249
+ const focusAnimation = useRef(new Animated.Value(0)).current;
250
+ const activeAnimation = useRef(new Animated.Value(Boolean(value) ? 1 : 0)).current;
251
+ const inputRef = useRef<TextInput>(null);
252
+
253
+ // Get color palette
254
+ const palette = colorPalette[color] || colorPalette.primary;
255
+
256
+ // Determine if we should show error colors
257
+ const effectiveColor = error ? 'error' : success ? 'success' : color;
258
+ const effectivePalette = colorPalette[effectiveColor] || colorPalette.primary;
259
+
260
+ // Get icon color based on focus state and error state
261
+ const iconColor = error
262
+ ? effectivePalette.main // Always show error color when there's an error
263
+ : focused
264
+ ? effectivePalette.main
265
+ : surfaceScale(0.62);
266
+
267
+ // Helper function to clone React elements with updated color
268
+ const cloneWithColor = (element: React.ReactNode, color: string): React.ReactNode => {
269
+ if (React.isValidElement(element) && element.type) {
270
+ return React.cloneElement(element, {
271
+ ...element.props,
272
+ color: color,
273
+ });
274
+ }
275
+ return element;
276
+ };
277
+
278
+ // Render leading/trailing elements
279
+ const leadingNode = typeof leading === 'function'
280
+ ? leading({ color: iconColor, size: 24 })
281
+ : cloneWithColor(leading, iconColor);
282
+
283
+ const trailingNode = typeof trailing === 'function'
284
+ ? trailing({ color: iconColor, size: 24 })
285
+ : cloneWithColor(trailing, iconColor);
286
+
287
+ // Debounced value for validation
288
+ const debouncedValue = useDebounce(internalValue, debounceMs);
289
+
290
+ // Password strength calculation
291
+ const passwordStrengthData = useMemo(() => {
292
+ if (passwordStrength && secureTextEntry && internalValue) {
293
+ return calculatePasswordStrength(internalValue);
294
+ }
295
+ return null;
296
+ }, [passwordStrength, secureTextEntry, internalValue]);
297
+
298
+ // Format input value
299
+ const formatInputValue = useCallback((text: string): string => {
300
+ if (formatValue) return formatValue(text);
301
+ if (inputMask && inputMask !== 'custom' && formatters[inputMask as keyof typeof formatters]) {
302
+ return formatters[inputMask as keyof typeof formatters](text);
303
+ }
304
+ if (customMask) return customMask(text);
305
+ return text;
306
+ }, [formatValue, inputMask, customMask]);
307
+
308
+ // Handle focus
309
+ const handleFocus = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
310
+ if (disabled) return;
311
+ setFocused(true);
312
+ onFocus?.(event);
313
+ }, [disabled, onFocus]);
314
+
315
+ // Handle blur
316
+ const handleBlur = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
317
+ setFocused(false);
318
+ onBlur?.(event);
319
+ }, [onBlur]);
320
+
321
+ // Handle mouse events
322
+ const handleMouseEnter = useCallback((event: NativeSyntheticEvent<TargetedEvent>) => {
323
+ onMouseEnter?.(event);
324
+ setHovered(true);
325
+ }, [onMouseEnter]);
326
+
327
+ const handleMouseLeave = useCallback((event: NativeSyntheticEvent<TargetedEvent>) => {
328
+ onMouseLeave?.(event);
329
+ setHovered(false);
330
+ }, [onMouseLeave]);
331
+
332
+ // Handle text change
333
+ const handleChangeText = useCallback((text: string) => {
334
+ const formattedText = formatInputValue(text);
335
+ setInternalValue(formattedText);
336
+ onChangeText?.(formattedText);
337
+ }, [formatInputValue, onChangeText]);
338
+
339
+ // Handle clear
340
+ const handleClear = useCallback(() => {
341
+ setInternalValue('');
342
+ onChangeText?.('');
343
+ onClear?.();
344
+ inputRef.current?.focus();
345
+ }, [onChangeText, onClear]);
346
+
347
+ // Toggle password visibility
348
+ const togglePasswordVisibility = useCallback(() => {
349
+ setShowPassword(prev => !prev);
350
+ }, []);
88
351
 
89
- // Animate border
90
- Animated.timing(borderAnim, {
91
- toValue: 1,
352
+ // Animate focus state
353
+ useEffect(() => {
354
+ Animated.timing(focusAnimation, {
355
+ toValue: focused ? 1 : 0,
92
356
  duration: 200,
93
357
  useNativeDriver: false,
94
358
  }).start();
95
- }, [onFocus, labelAnim, borderAnim]);
359
+ }, [focused, focusAnimation]);
96
360
 
97
- const handleBlur = useCallback(() => {
98
- setIsFocused(false);
99
- onBlur?.();
100
-
101
- // Animate border back
102
- Animated.timing(borderAnim, {
103
- toValue: 0,
361
+ // Animate active state (when focused or has value)
362
+ useEffect(() => {
363
+ const shouldBeActive = focused || Boolean(internalValue);
364
+ Animated.timing(activeAnimation, {
365
+ toValue: shouldBeActive ? 1 : 0,
104
366
  duration: 200,
105
367
  useNativeDriver: false,
106
368
  }).start();
369
+ }, [focused, internalValue, activeAnimation]);
107
370
 
108
- // Keep label at top if there's a value
109
- if (!value) {
110
- Animated.timing(labelAnim, {
111
- toValue: 0,
112
- duration: 200,
113
- useNativeDriver: false,
114
- }).start();
115
- }
116
- }, [onBlur, borderAnim, labelAnim, value]);
371
+ // Validation effect
372
+ useEffect(() => {
373
+ if (!validateOnChange || !onValidationChange) return;
117
374
 
118
- const handleChangeText = useCallback((text: string) => {
119
- onChangeText?.(text);
120
-
121
- // Animate label if value changes from empty to filled or vice versa
122
- const shouldShowLabel = text.length > 0;
123
-
124
- if (shouldShowLabel !== isLabelFloating) {
125
- setIsLabelFloating(shouldShowLabel);
126
- Animated.timing(labelAnim, {
127
- toValue: shouldShowLabel ? 1 : 0,
128
- duration: 200,
129
- useNativeDriver: false,
130
- }).start();
131
- }
132
- }, [onChangeText, labelAnim, isLabelFloating]);
375
+ const timer = setTimeout(() => {
376
+ setIsValidating(true);
377
+ const isValid = !error && debouncedValue.length > 0;
378
+ onValidationChange(isValid, debouncedValue);
379
+ setIsValidating(false);
380
+ }, 100);
381
+
382
+ return () => clearTimeout(timer);
383
+ }, [debouncedValue, validateOnChange, onValidationChange, error]);
133
384
 
134
- // Initialize label position based on current value
385
+ // Update internal value when prop changes
135
386
  useEffect(() => {
136
- if (value && !isLabelFloating) {
137
- setIsLabelFloating(true);
138
- labelAnim.setValue(1);
139
- }
140
- }, [value, isLabelFloating, labelAnim]);
387
+ setInternalValue(value);
388
+ }, [value]);
389
+
390
+ // Styles
391
+ const styles = useMemo(() => {
392
+ const isActive = focused || Boolean(internalValue);
393
+
394
+ return StyleSheet.create({
395
+ container: {
396
+ width: '100%',
397
+ marginBottom: 24,
398
+ },
399
+ inputContainer: {
400
+ flexDirection: 'row',
401
+ alignItems: 'center',
402
+ minHeight: variant === 'standard' ? 48 : 56,
403
+ backgroundColor: variant === 'filled'
404
+ ? focused
405
+ ? surfaceScale(0.08)
406
+ : hovered
407
+ ? surfaceScale(0.08)
408
+ : surfaceScale(0.04)
409
+ : 'transparent',
410
+ borderRadius: variant === 'standard' ? 0 : (variant === 'filled' ? 16 : 8),
411
+ borderTopLeftRadius: variant === 'filled' ? 16 : undefined,
412
+ borderTopRightRadius: variant === 'filled' ? 16 : undefined,
413
+ borderBottomLeftRadius: variant === 'filled' ? 0 : undefined,
414
+ borderBottomRightRadius: variant === 'filled' ? 0 : undefined,
415
+ borderWidth: variant === 'outlined' ? (focused ? 2 : 1) : 0,
416
+ borderColor: error
417
+ ? effectivePalette.main // Always show error color when there's an error
418
+ : focused
419
+ ? effectivePalette.main
420
+ : hovered
421
+ ? surfaceScale(0.87)
422
+ : surfaceScale(0.42),
423
+ position: 'relative',
424
+ ...Platform.select({
425
+ web: {
426
+ outlineStyle: 'none',
427
+ outlineWidth: 0,
428
+ outlineOffset: 0,
429
+ },
430
+ default: {},
431
+ }),
432
+ },
433
+ input: {
434
+ flex: 1,
435
+ minHeight: variant === 'standard' ? 48 : 56,
436
+ paddingStart: leadingNode ? 12 : variant === 'standard' ? 0 : 16,
437
+ paddingEnd: (trailingNode || clearable || secureTextEntry) ? 12 : variant === 'standard' ? 0 : 16,
438
+ paddingTop: variant === 'filled' && label ? 18 : 0,
439
+ color: surfaceScale(0.87),
440
+ fontSize: 16,
441
+ borderWidth: 0,
442
+ backgroundColor: 'transparent',
443
+ ...Platform.select({
444
+ web: {
445
+ border: 'none',
446
+ outlineStyle: 'none',
447
+ outlineWidth: 0,
448
+ outlineOffset: 0,
449
+ boxShadow: 'none',
450
+ '-webkit-appearance': 'none',
451
+ '-moz-appearance': 'none',
452
+ appearance: 'none',
453
+ },
454
+ default: {},
455
+ }),
456
+ },
457
+ leading: {
458
+ justifyContent: 'center',
459
+ alignItems: 'center',
460
+ width: 24,
461
+ height: 24,
462
+ marginStart: variant === 'standard' ? 0 : 12,
463
+ marginVertical: variant === 'standard' ? 12 : 16,
464
+ },
465
+ trailing: {
466
+ flexDirection: 'row',
467
+ justifyContent: 'center',
468
+ alignItems: 'center',
469
+ marginEnd: variant === 'standard' ? 0 : 12,
470
+ marginVertical: variant === 'standard' ? 12 : 16,
471
+ },
472
+ underline: {
473
+ position: 'absolute',
474
+ start: 0,
475
+ end: 0,
476
+ bottom: 0,
477
+ height: 1,
478
+ backgroundColor: error
479
+ ? effectivePalette.main // Always show error color when there's an error
480
+ : hovered
481
+ ? surfaceScale(0.87)
482
+ : surfaceScale(0.42),
483
+ },
484
+ underlineFocused: {
485
+ position: 'absolute',
486
+ start: 0,
487
+ end: 0,
488
+ bottom: 0,
489
+ height: 2,
490
+ backgroundColor: effectivePalette.main,
491
+ },
492
+ labelContainer: {
493
+ justifyContent: 'center',
494
+ position: 'absolute',
495
+ top: 0,
496
+ start: variant === 'standard' ? (leadingNode ? 36 : 0) : leadingNode ? 48 : 16,
497
+ height: variant === 'standard' ? 48 : 56,
498
+ },
499
+ label: {
500
+ fontSize: 16,
501
+ fontFamily: fontFamilies.phuduSemiBold,
502
+ color: surfaceScale(0.87),
503
+ },
504
+ helperText: {
505
+ fontSize: 12,
506
+ fontFamily: fontFamilies.phuduMedium,
507
+ marginTop: 4,
508
+ marginHorizontal: 16,
509
+ color: surfaceScale(0.6),
510
+ },
511
+
512
+ passwordStrengthContainer: {
513
+ marginTop: 8,
514
+ marginHorizontal: 16,
515
+ },
516
+ passwordStrengthBar: {
517
+ height: 4,
518
+ backgroundColor: '#E0E0E0',
519
+ borderRadius: 2,
520
+ overflow: 'hidden',
521
+ },
522
+ passwordStrengthFill: {
523
+ height: '100%',
524
+ borderRadius: 2,
525
+ },
526
+ passwordStrengthText: {
527
+ fontSize: 11,
528
+ fontWeight: '600',
529
+ marginTop: 4,
530
+ },
531
+ characterCount: {
532
+ fontSize: 11,
533
+ marginTop: 4,
534
+ marginHorizontal: 16,
535
+ textAlign: 'right',
536
+ color: surfaceScale(0.6),
537
+ },
538
+ clearButton: {
539
+ padding: 4,
540
+ marginLeft: 8,
541
+ },
542
+ passwordToggle: {
543
+ padding: 4,
544
+ marginLeft: 8,
545
+ },
546
+ validationIndicator: {
547
+ marginLeft: 8,
548
+ },
549
+ });
550
+ }, [variant, focused, hovered, effectivePalette, leadingNode, trailingNode, clearable, secureTextEntry, label, error, internalValue]);
551
+
552
+ // Character count display
553
+ const characterCount = internalValue.length;
554
+ const showCount = showCharacterCount && maxLength;
555
+
556
+ // Render password strength indicator
557
+ const renderPasswordStrength = () => {
558
+ if (!passwordStrengthData) return null;
141
559
 
142
- const togglePasswordVisibility = useCallback(() => {
143
- setShowPassword(!showPassword);
144
- }, [showPassword]);
145
-
146
- const getBorderColor = () => {
147
- if (error) return colors?.error || '#D32F2F';
148
- if (success) return colors?.success || '#2E7D32';
149
- if (isFocused) return colors?.primary || '#d169e5';
150
- return colors?.border || '#E0E0E0';
560
+ return (
561
+ <View style={styles.passwordStrengthContainer}>
562
+ <View style={styles.passwordStrengthBar}>
563
+ <View
564
+ style={[
565
+ styles.passwordStrengthFill,
566
+ {
567
+ width: `${passwordStrengthData.score}%`,
568
+ backgroundColor: passwordStrengthData.color
569
+ }
570
+ ]}
571
+ />
572
+ </View>
573
+ <Text style={[styles.passwordStrengthText, { color: passwordStrengthData.color }]}>
574
+ {passwordStrengthData.label}
575
+ </Text>
576
+ </View>
577
+ );
151
578
  };
152
579
 
153
- const getIconColor = () => {
154
- if (isFocused) return colors?.primary || '#d169e5';
155
- return iconColor || colors?.secondaryText || '#666666';
156
- };
580
+ // Render character count
581
+ const renderCharacterCount = () => {
582
+ if (!showCount) return null;
157
583
 
158
- const getLabelColor = () => {
159
- if (error) return colors?.error || '#D32F2F';
160
- if (isFocused) return colors?.primary || '#d169e5';
161
- return colors?.secondaryText || '#666666';
584
+ return (
585
+ <Text style={styles.characterCount}>
586
+ {characterCount}/{maxLength}
587
+ </Text>
588
+ );
162
589
  };
163
590
 
164
- const getBackgroundColor = () => {
165
- if (variant === 'filled') {
166
- return colors?.inputBackground || '#F5F5F5';
591
+ // Get helper text content (error takes precedence over helper text)
592
+ const helperTextContent = error || helperText;
593
+
594
+ // Render trailing elements
595
+ const renderTrailingElements = () => {
596
+ const elements = [];
597
+
598
+ // Loading indicator
599
+ if (isValidating) {
600
+ elements.push(
601
+ <ActivityIndicator
602
+ key="validating"
603
+ size="small"
604
+ color={effectivePalette.main}
605
+ style={styles.validationIndicator}
606
+ />
607
+ );
167
608
  }
168
- return 'transparent';
169
- };
170
609
 
171
- const styles = createStyles(colors, variant);
610
+ // Loading indicator
611
+ if (loading && !isValidating) {
612
+ elements.push(
613
+ <ActivityIndicator
614
+ key="loading"
615
+ size="small"
616
+ color={effectivePalette.main}
617
+ style={styles.validationIndicator}
618
+ />
619
+ );
620
+ }
172
621
 
173
- const BASE_PADDING = 20;
174
- const ICON_WIDTH = 22;
175
- const ICON_MARGIN = 12;
176
- const TEXT_LEFT = (icon || leftComponent) ? BASE_PADDING + ICON_WIDTH + ICON_MARGIN : BASE_PADDING;
177
- const FLOAT_LEFT_OFFSET = 10;
622
+ // Success indicator
623
+ if (success && !loading && !isValidating) {
624
+ elements.push(
625
+ <Ionicons
626
+ key="success"
627
+ name="checkmark-circle"
628
+ size={22}
629
+ color={colorPalette.success.main}
630
+ style={styles.validationIndicator}
631
+ />
632
+ );
633
+ }
178
634
 
179
- const isLabelFloated = Boolean(value || isFocused);
635
+ // Clear button
636
+ if (clearable && internalValue && !secureTextEntry) {
637
+ elements.push(
638
+ <TouchableOpacity
639
+ key="clear"
640
+ style={styles.clearButton}
641
+ onPress={handleClear}
642
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
643
+ accessibilityLabel="Clear input"
644
+ accessibilityRole="button"
645
+ >
646
+ <Ionicons
647
+ name="close-circle"
648
+ size={20}
649
+ color={iconColor}
650
+ />
651
+ </TouchableOpacity>
652
+ );
653
+ }
180
654
 
181
- // For web, make TextInput the primary element with absolute positioned decorations
182
- if (Platform.OS === 'web') {
183
- return (
184
- <View style={[styles.container, containerStyle]}>
185
- <View style={styles.webInputContainer}>
186
- {/* TextInput as the primary element */}
187
- <TextInput
188
- ref={ref}
189
- style={[
190
- styles.webInput,
191
- {
192
- color: colors?.text || '#000000',
193
- borderColor: 'transparent',
194
- backgroundColor: getBackgroundColor(),
195
- paddingLeft: TEXT_LEFT,
196
- paddingRight: 60, // Space for right components
197
- paddingTop: label ? 24 : 20, // Make room for floated label
198
- paddingBottom: 8,
199
- borderWidth: 0,
200
- ...Platform.select({
201
- web: { border: 'none', outline: 'none', boxShadow: 'none' },
202
- default: {},
203
- }),
204
- },
205
- inputStyle
206
- ]}
207
- onFocus={handleFocus}
208
- onBlur={handleBlur}
209
- onChangeText={handleChangeText}
210
- secureTextEntry={secureTextEntry && !showPassword}
211
- placeholderTextColor="transparent"
212
- testID={testID}
213
- autoComplete={secureTextEntry ? 'current-password' : 'off'}
214
- spellCheck={false}
215
- value={value}
216
- {...textInputProps}
655
+ // Password toggle
656
+ if (secureTextEntry) {
657
+ elements.push(
658
+ <TouchableOpacity
659
+ key="password"
660
+ style={styles.passwordToggle}
661
+ onPress={togglePasswordVisibility}
662
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
663
+ accessibilityLabel={showPassword ? "Hide password" : "Show password"}
664
+ accessibilityRole="button"
665
+ >
666
+ <Ionicons
667
+ name={showPassword ? "eye-off" : "eye"}
668
+ size={22}
669
+ color={iconColor}
217
670
  />
671
+ </TouchableOpacity>
672
+ );
673
+ }
674
+
675
+ return elements;
676
+ };
677
+
678
+ return (
679
+ <View
680
+ style={[styles.container, style]}
681
+ {...(Platform.OS === 'web' && { className: 'oxy-textfield-container' })}
682
+ >
683
+ <View
684
+ style={[
685
+ styles.inputContainer,
686
+ inputContainerStyle,
687
+ ]}
688
+ {...(Platform.OS === 'web' && {
689
+ onMouseEnter: handleMouseEnter,
690
+ onMouseLeave: handleMouseLeave,
691
+ })}
692
+ >
693
+ {/* Leading element */}
694
+ {leadingNode && (
695
+ <View style={[styles.leading, leadingContainerStyle]}>
696
+ {leadingNode}
697
+ </View>
698
+ )}
699
+
700
+ {/* Text Input */}
701
+ <TextInput
702
+ ref={(r) => {
703
+ if (typeof ref === 'function') {
704
+ ref(r);
705
+ } else if (ref && typeof ref === 'object') {
706
+ // @ts-ignore - React ref assignment
707
+ ref.current = r;
708
+ }
709
+ // @ts-ignore - Internal ref assignment
710
+ inputRef.current = r;
711
+ }}
712
+ style={[styles.input, inputStyle]}
713
+ placeholder={label ? (focused ? placeholder : undefined) : placeholder}
714
+ placeholderTextColor={surfaceScale(0.4)}
715
+ selectionColor={effectivePalette.main}
716
+ onFocus={handleFocus}
717
+ onBlur={handleBlur}
718
+ onChangeText={handleChangeText}
719
+ secureTextEntry={secureTextEntry && !showPassword}
720
+ value={internalValue}
721
+ editable={!disabled}
722
+ maxLength={maxLength}
723
+ {...(Platform.OS === 'web' && { className: 'oxy-textfield-input' })}
724
+ {...rest}
725
+ />
726
+
727
+ {/* Trailing elements */}
728
+ <View style={[styles.trailing, trailingContainerStyle]}>
729
+ {trailingNode}
730
+ {renderTrailingElements()}
731
+ </View>
218
732
 
219
- {/* SVG border with a gap for the floating label */}
220
- <Svg
221
- width={inputWidth}
222
- height={inputHeight}
223
- style={{ position: 'absolute', top: 0, left: 0, zIndex: 0 }}
224
- pointerEvents="none"
225
- >
226
- {/* Calculate the path for the border with rounded corners and a gap for the label */}
227
- <Path
228
- d={(() => {
229
- const y = borderWidth / 2;
230
- const x1 = borderRadius + borderWidth / 2;
231
- const x2 = inputWidth - borderRadius - borderWidth / 2;
232
- const labelGapStart = isLabelFloated ? labelLeft - 4 : x1;
233
- const labelGapEnd = isLabelFloated ? labelLeft + labelWidth + 4 : x2;
234
- // Start at left arc
235
- return `M${x1},${y}` +
236
- ` A${borderRadius},${borderRadius} 0 0 1 ${borderWidth / 2},${y + borderRadius}` +
237
- ` L${borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
238
- ` A${borderRadius},${borderRadius} 0 0 1 ${x1},${inputHeight - borderWidth / 2}` +
239
- ` L${x2},${inputHeight - borderWidth / 2}` +
240
- ` A${borderRadius},${borderRadius} 0 0 1 ${inputWidth - borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
241
- ` L${inputWidth - borderWidth / 2},${y + borderRadius}` +
242
- ` A${borderRadius},${borderRadius} 0 0 1 ${x2},${y}` +
243
- ` L${labelGapStart},${y}` +
244
- ` M${labelGapEnd},${y}` +
245
- ` L${x2},${y}`;
246
- })()}
247
- stroke={getBorderColor()}
248
- strokeWidth={borderWidth}
249
- fill="none"
733
+ {/* Underline for filled/standard variants */}
734
+ {(variant === 'filled' || variant === 'standard') && (
735
+ <>
736
+ <View style={styles.underline} pointerEvents="none" />
737
+ <Animated.View
738
+ style={[
739
+ styles.underlineFocused,
740
+ { transform: [{ scaleX: focusAnimation }] }
741
+ ]}
742
+ pointerEvents="none"
250
743
  />
251
- </Svg>
744
+ </>
745
+ )}
252
746
 
253
- {/* Floating label */}
254
- {label && (
747
+ {/* Label */}
748
+ {label && (
749
+ <View style={styles.labelContainer} pointerEvents="none">
255
750
  <Animated.Text
256
- onLayout={e => {
257
- setLabelWidth(e.nativeEvent.layout.width);
258
- setLabelLeft(e.nativeEvent.layout.x);
259
- }}
260
751
  style={[
261
- styles.webFloatingLabel,
752
+ styles.label,
262
753
  {
263
- color: getLabelColor(),
264
- left: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [TEXT_LEFT, FLOAT_LEFT_OFFSET] }),
265
- top: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [20, -14] }),
266
- fontSize: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [16, 12] }),
267
- backgroundColor: 'transparent',
268
- paddingHorizontal: 4,
269
- zIndex: 2,
754
+ color: focusAnimation.interpolate({
755
+ inputRange: [0, 1],
756
+ outputRange: [
757
+ error ? effectivePalette.main : surfaceScale(0.87),
758
+ effectivePalette.main
759
+ ],
760
+ }),
761
+ fontSize: activeAnimation.interpolate({
762
+ inputRange: [0, 1],
763
+ outputRange: [16, 12],
764
+ }),
765
+ transform: [
766
+ {
767
+ translateY: activeAnimation.interpolate({
768
+ inputRange: [0, 1],
769
+ outputRange: [0, variant === 'filled' ? -12 : variant === 'outlined' ? -28 : -24],
770
+ }),
771
+ },
772
+ ],
270
773
  },
271
- labelStyle
272
774
  ]}
273
775
  >
274
776
  {label}
275
777
  </Animated.Text>
276
- )}
277
-
278
- {/* Left Icon - positioned absolutely */}
279
- {icon && !leftComponent && (
280
- <View style={styles.webLeftIcon}>
281
- <Ionicons
282
- name={icon as any}
283
- size={22}
284
- color={getIconColor()}
285
- />
286
- </View>
287
- )}
288
-
289
- {/* Left Component - positioned absolutely */}
290
- {leftComponent && (
291
- <View style={styles.webLeftComponent}>
292
- {leftComponent}
293
- </View>
294
- )}
295
-
296
- {/* Right Components - positioned absolutely */}
297
- <View style={styles.webRightComponents}>
298
- {loading && (
299
- <ActivityIndicator
300
- size="small"
301
- color={colors?.primary || '#d169e5'}
302
- style={styles.validationIndicator}
303
- />
304
- )}
305
-
306
- {success && !loading && (
307
- <Ionicons
308
- name="checkmark-circle"
309
- size={22}
310
- color={colors?.success || '#2E7D32'}
311
- style={styles.validationIndicator}
312
- />
313
- )}
314
-
315
- {error && !loading && !success && (
316
- <Ionicons
317
- name="close-circle"
318
- size={22}
319
- color={colors?.error || '#D32F2F'}
320
- style={styles.validationIndicator}
321
- />
322
- )}
323
-
324
- {/* Password Toggle */}
325
- {secureTextEntry && (
326
- <TouchableOpacity
327
- style={styles.passwordToggle}
328
- onPress={togglePasswordVisibility}
329
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
330
- {...(Platform.OS === 'web' && {
331
- role: 'button',
332
- tabIndex: 0,
333
- onKeyPress: (e: any) => {
334
- if (e.key === 'Enter' || e.key === ' ') {
335
- e.preventDefault();
336
- togglePasswordVisibility();
337
- }
338
- },
339
- } as any)}
340
- >
341
- <Ionicons
342
- name={showPassword ? "eye-off" : "eye"}
343
- size={22}
344
- color={colors?.secondaryText || '#666666'}
345
- />
346
- </TouchableOpacity>
347
- )}
348
-
349
- {/* Custom Right Component */}
350
- {rightComponent}
351
- </View>
352
- </View>
353
-
354
- {/* Error Message */}
355
- {error && (
356
- <View style={[styles.errorContainer, errorStyle]}>
357
- <Ionicons
358
- name="alert-circle"
359
- size={16}
360
- color={colors?.error || '#D32F2F'}
361
- />
362
- <Text style={[
363
- styles.errorText,
364
- { color: colors?.error || '#D32F2F' }
365
- ]}>
366
- {error}
367
- </Text>
368
778
  </View>
369
779
  )}
370
780
  </View>
371
- );
372
- }
373
-
374
- // For mobile platforms, use Material Design structure
375
- return (
376
- <View style={[styles.container, containerStyle]}>
377
- <View
378
- style={[
379
- styles.inputWrapper,
380
- {
381
- borderColor: 'transparent',
382
- backgroundColor: getBackgroundColor(),
383
- borderWidth: 0,
384
- borderBottomWidth: variant === 'filled' ? 2 : (variant === 'outlined' ? 2 : 0),
385
- },
386
- ]}
387
- onLayout={(e: LayoutChangeEvent) => {
388
- setInputWidth(e.nativeEvent.layout.width);
389
- setInputHeight(e.nativeEvent.layout.height);
390
- }}
391
- >
392
- {/* Left Icon */}
393
- {icon && !leftComponent && (
394
- <Ionicons
395
- name={icon as any}
396
- size={22}
397
- color={getIconColor()}
398
- style={styles.inputIcon}
399
- />
400
- )}
401
781
 
402
- {/* Left Component */}
403
- {leftComponent}
404
-
405
- {/* Input Content */}
406
- <View style={styles.inputContent}>
407
- {label && (
408
- <>
409
- {/* SVG border with a gap for the floating label */}
410
- <Svg
411
- width={inputWidth}
412
- height={inputHeight}
413
- style={{ position: 'absolute', top: 0, left: 0, zIndex: 0 }}
414
- pointerEvents="none"
415
- >
416
- {/* Calculate the path for the border with rounded corners and a gap for the label */}
417
- <Path
418
- d={(() => {
419
- const y = borderWidth / 2;
420
- const x1 = borderRadius + borderWidth / 2;
421
- const x2 = inputWidth - borderRadius - borderWidth / 2;
422
- const labelGapStart = isLabelFloated ? labelLeft - 4 : x1;
423
- const labelGapEnd = isLabelFloated ? labelLeft + labelWidth + 4 : x2;
424
- // Start at left arc
425
- return `M${x1},${y}` +
426
- ` A${borderRadius},${borderRadius} 0 0 1 ${borderWidth / 2},${y + borderRadius}` +
427
- ` L${borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
428
- ` A${borderRadius},${borderRadius} 0 0 1 ${x1},${inputHeight - borderWidth / 2}` +
429
- ` L${x2},${inputHeight - borderWidth / 2}` +
430
- ` A${borderRadius},${borderRadius} 0 0 1 ${inputWidth - borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
431
- ` L${inputWidth - borderWidth / 2},${y + borderRadius}` +
432
- ` A${borderRadius},${borderRadius} 0 0 1 ${x2},${y}` +
433
- ` L${labelGapStart},${y}` +
434
- ` M${labelGapEnd},${y}` +
435
- ` L${x2},${y}`;
436
- })()}
437
- stroke={getBorderColor()}
438
- strokeWidth={borderWidth}
439
- fill="none"
440
- />
441
- </Svg>
442
- {/* Floating label */}
443
- <Animated.Text
444
- onLayout={e => {
445
- setLabelWidth(e.nativeEvent.layout.width);
446
- setLabelLeft(e.nativeEvent.layout.x);
447
- }}
448
- style={[
449
- styles.floatingLabel,
450
- {
451
- color: getLabelColor(),
452
- left: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [TEXT_LEFT, FLOAT_LEFT_OFFSET] }),
453
- top: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [20, -14] }),
454
- fontSize: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [16, 12] }),
455
- zIndex: 2,
456
- paddingHorizontal: 4,
457
- backgroundColor: 'transparent',
458
- },
459
- labelStyle
460
- ]}
461
- >
462
- {label}
463
- </Animated.Text>
464
- </>
465
- )}
466
- <TextInput
467
- ref={ref}
468
- style={[
469
- styles.input,
470
- {
471
- color: colors?.text || '#000000',
472
- backgroundColor: getBackgroundColor(),
473
- paddingLeft: TEXT_LEFT,
474
- paddingRight: 60, // Space for right components
475
- paddingTop: label ? 24 : 20, // Make room for floated label
476
- paddingBottom: 8,
477
- borderWidth: 0,
478
- borderColor: 'transparent',
479
- ...Platform.select({
480
- web: { border: 'none', outline: 'none', boxShadow: 'none' },
481
- default: {},
482
- }),
483
- },
484
- inputStyle
485
- ]}
486
- onFocus={handleFocus}
487
- onBlur={handleBlur}
488
- onChangeText={handleChangeText}
489
- secureTextEntry={secureTextEntry && !showPassword}
490
- placeholderTextColor="transparent"
491
- testID={testID}
492
- value={value}
493
- {...textInputProps}
494
- />
495
- </View>
782
+ {/* Helper text or error message */}
783
+ {helperTextContent && (
784
+ <Text style={[
785
+ styles.helperText,
786
+ error && { color: effectivePalette.main }
787
+ ]}>
788
+ {helperTextContent}
789
+ </Text>
790
+ )}
496
791
 
497
- {/* Right Components */}
498
- <View style={styles.rightComponents}>
499
- {loading && (
500
- <ActivityIndicator
501
- size="small"
502
- color={colors?.primary || '#d169e5'}
503
- style={styles.validationIndicator}
504
- />
505
- )}
506
-
507
- {success && !loading && (
508
- <Ionicons
509
- name="checkmark-circle"
510
- size={22}
511
- color={colors?.success || '#2E7D32'}
512
- style={styles.validationIndicator}
513
- />
514
- )}
515
-
516
- {error && !loading && !success && (
517
- <Ionicons
518
- name="close-circle"
519
- size={22}
520
- color={colors?.error || '#D32F2F'}
521
- style={styles.validationIndicator}
522
- />
523
- )}
524
-
525
- {/* Password Toggle */}
526
- {secureTextEntry && (
527
- <TouchableOpacity
528
- style={styles.passwordToggle}
529
- onPress={togglePasswordVisibility}
530
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
531
- >
532
- <Ionicons
533
- name={showPassword ? "eye-off" : "eye"}
534
- size={22}
535
- color={colors?.secondaryText || '#666666'}
536
- />
537
- </TouchableOpacity>
538
- )}
539
-
540
- {/* Custom Right Component */}
541
- {rightComponent}
542
- </View>
543
- </View>
792
+ {/* Password strength indicator */}
793
+ {renderPasswordStrength()}
544
794
 
545
- {/* Error Message */}
546
- {error && (
547
- <View style={[styles.errorContainer, errorStyle]}>
548
- <Ionicons
549
- name="alert-circle"
550
- size={16}
551
- color={colors?.error || '#D32F2F'}
552
- />
553
- <Text style={[
554
- styles.errorText,
555
- { color: colors?.error || '#D32F2F' }
556
- ]}>
557
- {error}
558
- </Text>
559
- </View>
560
- )}
561
- {/* Valid Message */}
562
- {!error && validMessage && (
563
- <View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 4, gap: 6 }}>
564
- <Ionicons name="checkmark-circle" size={16} color={colors?.success || '#2E7D32'} />
565
- <Text style={{ fontSize: 13, fontWeight: '500', color: colors?.success || '#2E7D32' }}>{validMessage}</Text>
566
- </View>
567
- )}
795
+ {/* Character count */}
796
+ {renderCharacterCount()}
568
797
  </View>
569
798
  );
570
799
  });
571
800
 
572
- const createStyles = (colors: any, variant: 'outlined' | 'filled') => StyleSheet.create({
573
- container: {
574
- width: '100%',
575
- marginBottom: 24,
576
- },
577
- inputWrapper: {
578
- flexDirection: 'row',
579
- alignItems: 'center',
580
- height: 64,
581
- borderTopLeftRadius: 16,
582
- borderTopRightRadius: 16,
583
- borderBottomLeftRadius: 0,
584
- borderBottomRightRadius: 0,
585
- paddingHorizontal: 20,
586
- backgroundColor: variant === 'filled' ? (colors?.inputBackground || '#F5F5F5') : 'transparent',
587
- position: 'relative',
588
- borderWidth: 0,
589
- borderColor: 'transparent',
590
- },
591
- inputIcon: {
592
- marginRight: 12,
593
- width: 22,
594
- height: 22,
595
- justifyContent: 'center',
596
- alignItems: 'center',
597
- },
598
- inputContent: {
599
- flex: 1,
600
- justifyContent: 'center',
601
- position: 'relative',
602
- height: 64,
603
- },
604
- floatingLabel: {
605
- position: 'absolute',
606
- fontWeight: '500',
607
- lineHeight: 24,
608
- backgroundColor: 'transparent',
609
- paddingHorizontal: 4,
610
- },
611
- input: {
612
- flex: 1,
613
- fontSize: 16,
614
- height: 24,
615
- paddingVertical: 0,
616
- marginTop: 8, // Space for floating label
617
- borderWidth: 0,
618
- borderColor: 'transparent',
619
- },
620
- // Web-specific styles
621
- webInputContainer: {
622
- position: 'relative',
623
- height: 64,
624
- },
625
- webInput: {
626
- width: '100%',
627
- height: 64,
628
- fontSize: 16,
629
- paddingHorizontal: 20,
630
- borderTopLeftRadius: 16,
631
- borderTopRightRadius: 16,
632
- borderBottomLeftRadius: 0,
633
- borderBottomRightRadius: 0,
634
- borderWidth: 0,
635
- borderColor: 'transparent',
636
- borderStyle: 'solid',
637
- },
638
- webFloatingLabel: {
639
- position: 'absolute',
640
- fontWeight: '500',
641
- lineHeight: 24,
642
- backgroundColor: 'transparent',
643
- paddingHorizontal: 4,
644
- },
645
- webLeftIcon: {
646
- position: 'absolute',
647
- left: 20,
648
- top: 21, // (64 - 22) / 2
649
- zIndex: 1,
650
- width: 22,
651
- height: 22,
652
- justifyContent: 'center',
653
- alignItems: 'center',
654
- },
655
- webLeftComponent: {
656
- position: 'absolute',
657
- left: 20,
658
- top: 0,
659
- height: 64,
660
- justifyContent: 'center',
661
- zIndex: 1,
662
- },
663
- webRightComponents: {
664
- position: 'absolute',
665
- right: 20,
666
- top: 0,
667
- height: 64,
668
- flexDirection: 'row',
669
- alignItems: 'center',
670
- zIndex: 1,
671
- },
672
- rightComponents: {
673
- flexDirection: 'row',
674
- alignItems: 'center',
675
- },
676
- validationIndicator: {
677
- marginLeft: 8,
678
- },
679
- passwordToggle: {
680
- padding: 4,
681
- marginLeft: 8,
682
- },
683
- errorContainer: {
684
- flexDirection: 'row',
685
- alignItems: 'center',
686
- padding: 12,
687
- borderRadius: 12,
688
- marginTop: 8,
689
- gap: 8,
690
- backgroundColor: (colors?.error || '#D32F2F') + '10',
691
- borderWidth: 1,
692
- borderColor: (colors?.error || '#D32F2F') + '30',
693
- },
694
- errorText: {
695
- fontSize: 12,
696
- fontWeight: '500',
697
- flex: 1,
698
- },
699
- });
700
-
701
801
  TextField.displayName = 'TextField';
702
802
 
703
803
  export default TextField;