create-du-app 0.1.3 → 0.1.4

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 (414) hide show
  1. package/README.md +10 -7
  2. package/package.json +6 -5
  3. package/src/index.js +8 -8
  4. package/src/prompts.js +1 -1
  5. package/templates/mobile/expo/.env.example +5 -0
  6. package/templates/mobile/expo/.eslintrc.js +7 -0
  7. package/templates/mobile/expo/.prettierrc.js +7 -0
  8. package/templates/mobile/expo/.svgrrc.js +9 -0
  9. package/templates/mobile/expo/README.md +42 -7
  10. package/templates/mobile/expo/_gitignore +20 -0
  11. package/templates/mobile/expo/_package.json +62 -1
  12. package/templates/mobile/expo/app.json +18 -0
  13. package/templates/mobile/expo/babel.config.js +21 -0
  14. package/templates/mobile/expo/index.js +5 -0
  15. package/templates/mobile/expo/metro.config.js +31 -0
  16. package/templates/mobile/expo/src/app/App.tsx +24 -0
  17. package/templates/mobile/expo/src/app/app-provider.tsx +36 -0
  18. package/templates/mobile/expo/src/app/config/translation.ts +26 -0
  19. package/templates/mobile/expo/src/app/index.ts +1 -0
  20. package/templates/mobile/expo/src/app/navigation/app-route-type.ts +27 -0
  21. package/templates/mobile/expo/src/app/navigation/bottom-tabs.tsx +34 -0
  22. package/templates/mobile/expo/src/app/navigation/index.tsx +14 -0
  23. package/templates/mobile/expo/src/app/stores/auth.store.ts +33 -0
  24. package/templates/mobile/expo/src/app/stores/common.store.ts +19 -0
  25. package/templates/mobile/expo/src/app/stores/index.ts +3 -0
  26. package/templates/mobile/expo/src/app/stores/loading.store.ts +22 -0
  27. package/templates/mobile/expo/src/assets/Images/empty-list.png +0 -0
  28. package/templates/mobile/expo/src/assets/Images/index.ts +5 -0
  29. package/templates/mobile/expo/src/assets/Images/screen-bg-gradian.png +0 -0
  30. package/templates/mobile/expo/src/assets/i18n/en.json +12 -0
  31. package/templates/mobile/expo/src/assets/i18n/fr.json +12 -0
  32. package/templates/mobile/expo/src/assets/lotties/index.ts +3 -0
  33. package/templates/mobile/expo/src/assets/lotties/loading.json +1 -0
  34. package/templates/mobile/expo/src/assets/svgs/arrow-left.svg +3 -0
  35. package/templates/mobile/expo/src/assets/svgs/arrow-right.svg +3 -0
  36. package/templates/mobile/expo/src/assets/svgs/calendar.svg +12 -0
  37. package/templates/mobile/expo/src/assets/svgs/check.svg +3 -0
  38. package/templates/mobile/expo/src/assets/svgs/close.svg +3 -0
  39. package/templates/mobile/expo/src/assets/svgs/eye-hide.svg +3 -0
  40. package/templates/mobile/expo/src/assets/svgs/eye.svg +4 -0
  41. package/templates/mobile/expo/src/assets/svgs/index.ts +29 -0
  42. package/templates/mobile/expo/src/assets/svgs/minus.svg +3 -0
  43. package/templates/mobile/expo/src/assets/svgs/plus.svg +3 -0
  44. package/templates/mobile/expo/src/assets/svgs/search.svg +3 -0
  45. package/templates/mobile/expo/src/assets/svgs/toast-error.svg +5 -0
  46. package/templates/mobile/expo/src/assets/svgs/toast-success.svg +4 -0
  47. package/templates/mobile/expo/src/core/api/endpoints.ts +8 -0
  48. package/templates/mobile/expo/src/core/api/example.api.ts +53 -0
  49. package/templates/mobile/expo/src/core/api/index.ts +4 -0
  50. package/templates/mobile/expo/src/core/components/common/index.ts +1 -0
  51. package/templates/mobile/expo/src/core/components/common/list-empty/index.ts +2 -0
  52. package/templates/mobile/expo/src/core/components/common/list-empty/list-empty.tsx +30 -0
  53. package/templates/mobile/expo/src/core/components/common/list-empty/list-empty.type.ts +5 -0
  54. package/templates/mobile/expo/src/core/components/forms/hf-date-time.tsx +301 -0
  55. package/templates/mobile/expo/src/core/components/forms/hf-password-input.tsx +153 -0
  56. package/templates/mobile/expo/src/core/components/forms/hf-text-input.tsx +59 -0
  57. package/templates/mobile/expo/src/core/components/forms/hf-time-picker.tsx +117 -0
  58. package/templates/mobile/expo/src/core/components/forms/index.ts +4 -0
  59. package/templates/mobile/expo/src/core/components/index.ts +5 -0
  60. package/templates/mobile/expo/src/core/components/loading/index.ts +1 -0
  61. package/templates/mobile/expo/src/core/components/loading/loading-app/index.ts +1 -0
  62. package/templates/mobile/expo/src/core/components/loading/loading-app/loading-app.tsx +50 -0
  63. package/templates/mobile/expo/src/core/components/offline/index.ts +1 -0
  64. package/templates/mobile/expo/src/core/components/offline/offline-banner.tsx +186 -0
  65. package/templates/mobile/expo/src/core/components/screen/index.ts +1 -0
  66. package/templates/mobile/expo/src/core/components/screen/screen-container/index.ts +1 -0
  67. package/templates/mobile/expo/src/core/components/screen/screen-container/screen-container.tsx +252 -0
  68. package/templates/mobile/expo/src/core/components/splash/index.ts +1 -0
  69. package/templates/mobile/expo/src/core/components/splash/splash-overlay/index.ts +1 -0
  70. package/templates/mobile/expo/src/core/components/splash/splash-overlay/splash-overlay.tsx +93 -0
  71. package/templates/mobile/expo/src/core/components/ui/animated-list-item/animated-list-item.tsx +48 -0
  72. package/templates/mobile/expo/src/core/components/ui/animated-list-item/animated-list-item.type.ts +10 -0
  73. package/templates/mobile/expo/src/core/components/ui/animated-list-item/index.ts +2 -0
  74. package/templates/mobile/expo/src/core/components/ui/app-image/app-image.tsx +104 -0
  75. package/templates/mobile/expo/src/core/components/ui/app-image/app-image.type.ts +19 -0
  76. package/templates/mobile/expo/src/core/components/ui/app-image/index.ts +2 -0
  77. package/templates/mobile/expo/src/core/components/ui/asset-placeholder/asset-placeholder.tsx +76 -0
  78. package/templates/mobile/expo/src/core/components/ui/asset-placeholder/index.ts +1 -0
  79. package/templates/mobile/expo/src/core/components/ui/avatar-image/avatar-image.tsx +90 -0
  80. package/templates/mobile/expo/src/core/components/ui/avatar-image/index.ts +1 -0
  81. package/templates/mobile/expo/src/core/components/ui/bottom-sheet/bottom-sheet.tsx +145 -0
  82. package/templates/mobile/expo/src/core/components/ui/bottom-sheet/bottom-sheet.type.ts +10 -0
  83. package/templates/mobile/expo/src/core/components/ui/bottom-sheet/index.ts +2 -0
  84. package/templates/mobile/expo/src/core/components/ui/button/button.style.ts +146 -0
  85. package/templates/mobile/expo/src/core/components/ui/button/button.tsx +97 -0
  86. package/templates/mobile/expo/src/core/components/ui/button/button.type.ts +47 -0
  87. package/templates/mobile/expo/src/core/components/ui/button/index.ts +4 -0
  88. package/templates/mobile/expo/src/core/components/ui/checkbox/checkbox.tsx +127 -0
  89. package/templates/mobile/expo/src/core/components/ui/checkbox/checkbox.type.ts +24 -0
  90. package/templates/mobile/expo/src/core/components/ui/checkbox/index.ts +2 -0
  91. package/templates/mobile/expo/src/core/components/ui/collapsible-section/collapsible-section.tsx +141 -0
  92. package/templates/mobile/expo/src/core/components/ui/collapsible-section/collapsible-section.type.ts +10 -0
  93. package/templates/mobile/expo/src/core/components/ui/collapsible-section/index.ts +2 -0
  94. package/templates/mobile/expo/src/core/components/ui/components.registry.json +313 -0
  95. package/templates/mobile/expo/src/core/components/ui/field/field-frame.tsx +62 -0
  96. package/templates/mobile/expo/src/core/components/ui/field/field.shared.ts +31 -0
  97. package/templates/mobile/expo/src/core/components/ui/header/header.tsx +196 -0
  98. package/templates/mobile/expo/src/core/components/ui/header/header.type.ts +30 -0
  99. package/templates/mobile/expo/src/core/components/ui/header/index.ts +2 -0
  100. package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.style.ts +23 -0
  101. package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.tsx +66 -0
  102. package/templates/mobile/expo/src/core/components/ui/icon-button/icon-button.type.ts +25 -0
  103. package/templates/mobile/expo/src/core/components/ui/icon-button/index.ts +2 -0
  104. package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.tsx +107 -0
  105. package/templates/mobile/expo/src/core/components/ui/image-slider/image-slider.type.ts +10 -0
  106. package/templates/mobile/expo/src/core/components/ui/image-slider/index.ts +2 -0
  107. package/templates/mobile/expo/src/core/components/ui/index.ts +25 -0
  108. package/templates/mobile/expo/src/core/components/ui/label/index.ts +2 -0
  109. package/templates/mobile/expo/src/core/components/ui/label/label.tsx +36 -0
  110. package/templates/mobile/expo/src/core/components/ui/label/label.type.ts +12 -0
  111. package/templates/mobile/expo/src/core/components/ui/modal/index.ts +2 -0
  112. package/templates/mobile/expo/src/core/components/ui/modal/modal.tsx +62 -0
  113. package/templates/mobile/expo/src/core/components/ui/modal/modal.type.ts +11 -0
  114. package/templates/mobile/expo/src/core/components/ui/otp-input/index.ts +2 -0
  115. package/templates/mobile/expo/src/core/components/ui/otp-input/otp-input.tsx +129 -0
  116. package/templates/mobile/expo/src/core/components/ui/otp-input/otp-input.type.ts +20 -0
  117. package/templates/mobile/expo/src/core/components/ui/page-dots/index.ts +1 -0
  118. package/templates/mobile/expo/src/core/components/ui/page-dots/page-dots.tsx +60 -0
  119. package/templates/mobile/expo/src/core/components/ui/radio/index.ts +2 -0
  120. package/templates/mobile/expo/src/core/components/ui/radio/radio.tsx +121 -0
  121. package/templates/mobile/expo/src/core/components/ui/radio/radio.type.ts +20 -0
  122. package/templates/mobile/expo/src/core/components/ui/screen/index.ts +1 -0
  123. package/templates/mobile/expo/src/core/components/ui/screen/screen-gradient.tsx +33 -0
  124. package/templates/mobile/expo/src/core/components/ui/search-box/index.ts +2 -0
  125. package/templates/mobile/expo/src/core/components/ui/search-box/search-box.tsx +162 -0
  126. package/templates/mobile/expo/src/core/components/ui/search-box/search-box.type.ts +26 -0
  127. package/templates/mobile/expo/src/core/components/ui/segmented-control/index.ts +2 -0
  128. package/templates/mobile/expo/src/core/components/ui/segmented-control/segmented-control.tsx +86 -0
  129. package/templates/mobile/expo/src/core/components/ui/segmented-control/segmented-control.type.ts +22 -0
  130. package/templates/mobile/expo/src/core/components/ui/skeleton/index.ts +2 -0
  131. package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.tsx +106 -0
  132. package/templates/mobile/expo/src/core/components/ui/skeleton/skeleton.type.ts +8 -0
  133. package/templates/mobile/expo/src/core/components/ui/success-state/index.ts +1 -0
  134. package/templates/mobile/expo/src/core/components/ui/success-state/success-state.tsx +68 -0
  135. package/templates/mobile/expo/src/core/components/ui/tabs/index.ts +2 -0
  136. package/templates/mobile/expo/src/core/components/ui/tabs/tabs.tsx +273 -0
  137. package/templates/mobile/expo/src/core/components/ui/tabs/tabs.type.ts +21 -0
  138. package/templates/mobile/expo/src/core/components/ui/tag-input/index.ts +2 -0
  139. package/templates/mobile/expo/src/core/components/ui/tag-input/tag-input.tsx +146 -0
  140. package/templates/mobile/expo/src/core/components/ui/tag-input/tag-input.type.ts +22 -0
  141. package/templates/mobile/expo/src/core/components/ui/text-area/index.ts +2 -0
  142. package/templates/mobile/expo/src/core/components/ui/text-area/text-area.tsx +90 -0
  143. package/templates/mobile/expo/src/core/components/ui/text-area/text-area.type.ts +20 -0
  144. package/templates/mobile/expo/src/core/components/ui/text-field/index.ts +2 -0
  145. package/templates/mobile/expo/src/core/components/ui/text-field/text-field.tsx +116 -0
  146. package/templates/mobile/expo/src/core/components/ui/text-field/text-field.type.ts +21 -0
  147. package/templates/mobile/expo/src/core/components/ui/toggle/index.ts +2 -0
  148. package/templates/mobile/expo/src/core/components/ui/toggle/toggle.tsx +110 -0
  149. package/templates/mobile/expo/src/core/components/ui/toggle/toggle.type.ts +19 -0
  150. package/templates/mobile/expo/src/core/constants/external-urls.constant.ts +5 -0
  151. package/templates/mobile/expo/src/core/constants/hard-data.constant.ts +0 -0
  152. package/templates/mobile/expo/src/core/constants/index.ts +2 -0
  153. package/templates/mobile/expo/src/core/constants/type.constant.ts +3 -0
  154. package/templates/mobile/expo/src/core/context/index.ts +1 -0
  155. package/templates/mobile/expo/src/core/context/shared-transition-context.tsx +35 -0
  156. package/templates/mobile/expo/src/core/hook/index.ts +8 -0
  157. package/templates/mobile/expo/src/core/hook/useActiveRouteName.ts +63 -0
  158. package/templates/mobile/expo/src/core/hook/useAppNavigation.tsx +7 -0
  159. package/templates/mobile/expo/src/core/hook/useBottomInset.tsx +26 -0
  160. package/templates/mobile/expo/src/core/hook/useDebounce.tsx +16 -0
  161. package/templates/mobile/expo/src/core/hook/useEndReached.tsx +46 -0
  162. package/templates/mobile/expo/src/core/hook/useManualRefetch.ts +56 -0
  163. package/templates/mobile/expo/src/core/hook/useNetworkStatus.ts +68 -0
  164. package/templates/mobile/expo/src/core/hook/useTimeout.tsx +30 -0
  165. package/templates/mobile/expo/src/core/index.ts +7 -0
  166. package/templates/mobile/expo/src/core/services/api.service.ts +230 -0
  167. package/templates/mobile/expo/src/core/services/device-id.service.ts +23 -0
  168. package/templates/mobile/expo/src/core/services/index.ts +3 -0
  169. package/templates/mobile/expo/src/core/services/session-end.bridge.ts +26 -0
  170. package/templates/mobile/expo/src/core/theme/dark.theme.ts +10 -0
  171. package/templates/mobile/expo/src/core/theme/index.ts +5 -0
  172. package/templates/mobile/expo/src/core/theme/light.theme.ts +44 -0
  173. package/templates/mobile/expo/src/core/theme/theme-context.tsx +82 -0
  174. package/templates/mobile/expo/src/core/theme/theme.types.ts +26 -0
  175. package/templates/mobile/expo/src/core/theme/use-themed-styles.ts +25 -0
  176. package/templates/mobile/expo/src/core/utils/color.util.tsx +198 -0
  177. package/templates/mobile/expo/src/core/utils/date.util.ts +97 -0
  178. package/templates/mobile/expo/src/core/utils/device-locale.util.ts +22 -0
  179. package/templates/mobile/expo/src/core/utils/emitter/index.ts +161 -0
  180. package/templates/mobile/expo/src/core/utils/emitter/type.ts +40 -0
  181. package/templates/mobile/expo/src/core/utils/enum.util.tsx +15 -0
  182. package/templates/mobile/expo/src/core/utils/font.util.tsx +42 -0
  183. package/templates/mobile/expo/src/core/utils/func.util.ts +48 -0
  184. package/templates/mobile/expo/src/core/utils/greeting.util.ts +20 -0
  185. package/templates/mobile/expo/src/core/utils/image-format.util.ts +117 -0
  186. package/templates/mobile/expo/src/core/utils/image-picker.util.ts +84 -0
  187. package/templates/mobile/expo/src/core/utils/index.ts +18 -0
  188. package/templates/mobile/expo/src/core/utils/linking.util.ts +16 -0
  189. package/templates/mobile/expo/src/core/utils/navigation.util.tsx +100 -0
  190. package/templates/mobile/expo/src/core/utils/number-format.ts +28 -0
  191. package/templates/mobile/expo/src/core/utils/query-client.util.ts +35 -0
  192. package/templates/mobile/expo/src/core/utils/query-persister.util.ts +36 -0
  193. package/templates/mobile/expo/src/core/utils/schema.util.tsx +2713 -0
  194. package/templates/mobile/expo/src/core/utils/size.util.tsx +74 -0
  195. package/templates/mobile/expo/src/core/utils/storage.util.tsx +53 -0
  196. package/templates/mobile/expo/src/core/utils/toast.util.tsx +151 -0
  197. package/templates/mobile/expo/src/core/utils/translator.util.tsx +23 -0
  198. package/templates/mobile/expo/src/core/utils/typography.util.tsx +69 -0
  199. package/templates/mobile/expo/src/core/utils/validate.util.tsx +13 -0
  200. package/templates/mobile/expo/src/declarations.d.ts +54 -0
  201. package/templates/mobile/expo/src/modules/home/home.screen.tsx +33 -0
  202. package/templates/mobile/expo/src/modules/profile/profile.screen.tsx +29 -0
  203. package/templates/mobile/expo/src/scripts/link-fonts.js +60 -0
  204. package/templates/mobile/expo/src/scripts/sync-images.js +56 -0
  205. package/templates/mobile/expo/src/scripts/sync-svgs.js +50 -0
  206. package/templates/mobile/expo/src/types/models.d.ts +24 -0
  207. package/templates/mobile/expo/tsconfig.json +19 -0
  208. package/templates/mobile/rn/.env.example +5 -0
  209. package/templates/mobile/rn/.eslintrc.js +7 -0
  210. package/templates/mobile/rn/.prettierrc.js +7 -0
  211. package/templates/mobile/rn/.svgrrc.js +9 -0
  212. package/templates/mobile/rn/README.md +40 -7
  213. package/templates/mobile/rn/_gitignore +24 -0
  214. package/templates/mobile/rn/_package.json +67 -1
  215. package/templates/mobile/rn/app.json +4 -0
  216. package/templates/mobile/rn/babel.config.js +18 -0
  217. package/templates/mobile/rn/index.js +8 -0
  218. package/templates/mobile/rn/metro.config.js +33 -0
  219. package/templates/mobile/rn/src/app/App.tsx +24 -0
  220. package/templates/mobile/rn/src/app/app-provider.tsx +36 -0
  221. package/templates/mobile/rn/src/app/config/translation.ts +26 -0
  222. package/templates/mobile/rn/src/app/index.ts +1 -0
  223. package/templates/mobile/rn/src/app/navigation/app-route-type.ts +27 -0
  224. package/templates/mobile/rn/src/app/navigation/bottom-tabs.tsx +34 -0
  225. package/templates/mobile/rn/src/app/navigation/index.tsx +14 -0
  226. package/templates/mobile/rn/src/app/stores/auth.store.ts +33 -0
  227. package/templates/mobile/rn/src/app/stores/common.store.ts +19 -0
  228. package/templates/mobile/rn/src/app/stores/index.ts +3 -0
  229. package/templates/mobile/rn/src/app/stores/loading.store.ts +22 -0
  230. package/templates/mobile/rn/src/assets/Images/empty-list.png +0 -0
  231. package/templates/mobile/rn/src/assets/Images/index.ts +5 -0
  232. package/templates/mobile/rn/src/assets/Images/screen-bg-gradian.png +0 -0
  233. package/templates/mobile/rn/src/assets/i18n/en.json +12 -0
  234. package/templates/mobile/rn/src/assets/i18n/fr.json +12 -0
  235. package/templates/mobile/rn/src/assets/lotties/index.ts +3 -0
  236. package/templates/mobile/rn/src/assets/lotties/loading.json +1 -0
  237. package/templates/mobile/rn/src/assets/svgs/arrow-left.svg +3 -0
  238. package/templates/mobile/rn/src/assets/svgs/arrow-right.svg +3 -0
  239. package/templates/mobile/rn/src/assets/svgs/calendar.svg +12 -0
  240. package/templates/mobile/rn/src/assets/svgs/check.svg +3 -0
  241. package/templates/mobile/rn/src/assets/svgs/close.svg +3 -0
  242. package/templates/mobile/rn/src/assets/svgs/eye-hide.svg +3 -0
  243. package/templates/mobile/rn/src/assets/svgs/eye.svg +4 -0
  244. package/templates/mobile/rn/src/assets/svgs/index.ts +29 -0
  245. package/templates/mobile/rn/src/assets/svgs/minus.svg +3 -0
  246. package/templates/mobile/rn/src/assets/svgs/plus.svg +3 -0
  247. package/templates/mobile/rn/src/assets/svgs/search.svg +3 -0
  248. package/templates/mobile/rn/src/assets/svgs/toast-error.svg +5 -0
  249. package/templates/mobile/rn/src/assets/svgs/toast-success.svg +4 -0
  250. package/templates/mobile/rn/src/core/api/endpoints.ts +8 -0
  251. package/templates/mobile/rn/src/core/api/example.api.ts +53 -0
  252. package/templates/mobile/rn/src/core/api/index.ts +4 -0
  253. package/templates/mobile/rn/src/core/components/common/index.ts +1 -0
  254. package/templates/mobile/rn/src/core/components/common/list-empty/index.ts +2 -0
  255. package/templates/mobile/rn/src/core/components/common/list-empty/list-empty.tsx +30 -0
  256. package/templates/mobile/rn/src/core/components/common/list-empty/list-empty.type.ts +5 -0
  257. package/templates/mobile/rn/src/core/components/forms/hf-date-time.tsx +301 -0
  258. package/templates/mobile/rn/src/core/components/forms/hf-password-input.tsx +153 -0
  259. package/templates/mobile/rn/src/core/components/forms/hf-text-input.tsx +59 -0
  260. package/templates/mobile/rn/src/core/components/forms/hf-time-picker.tsx +117 -0
  261. package/templates/mobile/rn/src/core/components/forms/index.ts +4 -0
  262. package/templates/mobile/rn/src/core/components/index.ts +5 -0
  263. package/templates/mobile/rn/src/core/components/loading/index.ts +1 -0
  264. package/templates/mobile/rn/src/core/components/loading/loading-app/index.ts +1 -0
  265. package/templates/mobile/rn/src/core/components/loading/loading-app/loading-app.tsx +50 -0
  266. package/templates/mobile/rn/src/core/components/offline/index.ts +1 -0
  267. package/templates/mobile/rn/src/core/components/offline/offline-banner.tsx +186 -0
  268. package/templates/mobile/rn/src/core/components/screen/index.ts +1 -0
  269. package/templates/mobile/rn/src/core/components/screen/screen-container/index.ts +1 -0
  270. package/templates/mobile/rn/src/core/components/screen/screen-container/screen-container.tsx +252 -0
  271. package/templates/mobile/rn/src/core/components/splash/index.ts +1 -0
  272. package/templates/mobile/rn/src/core/components/splash/splash-overlay/index.ts +1 -0
  273. package/templates/mobile/rn/src/core/components/splash/splash-overlay/splash-overlay.tsx +93 -0
  274. package/templates/mobile/rn/src/core/components/ui/animated-list-item/animated-list-item.tsx +48 -0
  275. package/templates/mobile/rn/src/core/components/ui/animated-list-item/animated-list-item.type.ts +10 -0
  276. package/templates/mobile/rn/src/core/components/ui/animated-list-item/index.ts +2 -0
  277. package/templates/mobile/rn/src/core/components/ui/app-image/app-image.tsx +104 -0
  278. package/templates/mobile/rn/src/core/components/ui/app-image/app-image.type.ts +19 -0
  279. package/templates/mobile/rn/src/core/components/ui/app-image/index.ts +2 -0
  280. package/templates/mobile/rn/src/core/components/ui/asset-placeholder/asset-placeholder.tsx +76 -0
  281. package/templates/mobile/rn/src/core/components/ui/asset-placeholder/index.ts +1 -0
  282. package/templates/mobile/rn/src/core/components/ui/avatar-image/avatar-image.tsx +90 -0
  283. package/templates/mobile/rn/src/core/components/ui/avatar-image/index.ts +1 -0
  284. package/templates/mobile/rn/src/core/components/ui/bottom-sheet/bottom-sheet.tsx +145 -0
  285. package/templates/mobile/rn/src/core/components/ui/bottom-sheet/bottom-sheet.type.ts +10 -0
  286. package/templates/mobile/rn/src/core/components/ui/bottom-sheet/index.ts +2 -0
  287. package/templates/mobile/rn/src/core/components/ui/button/button.style.ts +146 -0
  288. package/templates/mobile/rn/src/core/components/ui/button/button.tsx +97 -0
  289. package/templates/mobile/rn/src/core/components/ui/button/button.type.ts +47 -0
  290. package/templates/mobile/rn/src/core/components/ui/button/index.ts +4 -0
  291. package/templates/mobile/rn/src/core/components/ui/checkbox/checkbox.tsx +127 -0
  292. package/templates/mobile/rn/src/core/components/ui/checkbox/checkbox.type.ts +24 -0
  293. package/templates/mobile/rn/src/core/components/ui/checkbox/index.ts +2 -0
  294. package/templates/mobile/rn/src/core/components/ui/collapsible-section/collapsible-section.tsx +141 -0
  295. package/templates/mobile/rn/src/core/components/ui/collapsible-section/collapsible-section.type.ts +10 -0
  296. package/templates/mobile/rn/src/core/components/ui/collapsible-section/index.ts +2 -0
  297. package/templates/mobile/rn/src/core/components/ui/components.registry.json +313 -0
  298. package/templates/mobile/rn/src/core/components/ui/field/field-frame.tsx +62 -0
  299. package/templates/mobile/rn/src/core/components/ui/field/field.shared.ts +31 -0
  300. package/templates/mobile/rn/src/core/components/ui/header/header.tsx +196 -0
  301. package/templates/mobile/rn/src/core/components/ui/header/header.type.ts +30 -0
  302. package/templates/mobile/rn/src/core/components/ui/header/index.ts +2 -0
  303. package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.style.ts +23 -0
  304. package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.tsx +66 -0
  305. package/templates/mobile/rn/src/core/components/ui/icon-button/icon-button.type.ts +25 -0
  306. package/templates/mobile/rn/src/core/components/ui/icon-button/index.ts +2 -0
  307. package/templates/mobile/rn/src/core/components/ui/image-slider/image-slider.tsx +107 -0
  308. package/templates/mobile/rn/src/core/components/ui/image-slider/image-slider.type.ts +10 -0
  309. package/templates/mobile/rn/src/core/components/ui/image-slider/index.ts +2 -0
  310. package/templates/mobile/rn/src/core/components/ui/index.ts +25 -0
  311. package/templates/mobile/rn/src/core/components/ui/label/index.ts +2 -0
  312. package/templates/mobile/rn/src/core/components/ui/label/label.tsx +36 -0
  313. package/templates/mobile/rn/src/core/components/ui/label/label.type.ts +12 -0
  314. package/templates/mobile/rn/src/core/components/ui/modal/index.ts +2 -0
  315. package/templates/mobile/rn/src/core/components/ui/modal/modal.tsx +62 -0
  316. package/templates/mobile/rn/src/core/components/ui/modal/modal.type.ts +11 -0
  317. package/templates/mobile/rn/src/core/components/ui/otp-input/index.ts +2 -0
  318. package/templates/mobile/rn/src/core/components/ui/otp-input/otp-input.tsx +129 -0
  319. package/templates/mobile/rn/src/core/components/ui/otp-input/otp-input.type.ts +20 -0
  320. package/templates/mobile/rn/src/core/components/ui/page-dots/index.ts +1 -0
  321. package/templates/mobile/rn/src/core/components/ui/page-dots/page-dots.tsx +60 -0
  322. package/templates/mobile/rn/src/core/components/ui/radio/index.ts +2 -0
  323. package/templates/mobile/rn/src/core/components/ui/radio/radio.tsx +121 -0
  324. package/templates/mobile/rn/src/core/components/ui/radio/radio.type.ts +20 -0
  325. package/templates/mobile/rn/src/core/components/ui/screen/index.ts +1 -0
  326. package/templates/mobile/rn/src/core/components/ui/screen/screen-gradient.tsx +33 -0
  327. package/templates/mobile/rn/src/core/components/ui/search-box/index.ts +2 -0
  328. package/templates/mobile/rn/src/core/components/ui/search-box/search-box.tsx +162 -0
  329. package/templates/mobile/rn/src/core/components/ui/search-box/search-box.type.ts +26 -0
  330. package/templates/mobile/rn/src/core/components/ui/segmented-control/index.ts +2 -0
  331. package/templates/mobile/rn/src/core/components/ui/segmented-control/segmented-control.tsx +86 -0
  332. package/templates/mobile/rn/src/core/components/ui/segmented-control/segmented-control.type.ts +22 -0
  333. package/templates/mobile/rn/src/core/components/ui/skeleton/index.ts +2 -0
  334. package/templates/mobile/rn/src/core/components/ui/skeleton/skeleton.tsx +106 -0
  335. package/templates/mobile/rn/src/core/components/ui/skeleton/skeleton.type.ts +8 -0
  336. package/templates/mobile/rn/src/core/components/ui/success-state/index.ts +1 -0
  337. package/templates/mobile/rn/src/core/components/ui/success-state/success-state.tsx +68 -0
  338. package/templates/mobile/rn/src/core/components/ui/tabs/index.ts +2 -0
  339. package/templates/mobile/rn/src/core/components/ui/tabs/tabs.tsx +273 -0
  340. package/templates/mobile/rn/src/core/components/ui/tabs/tabs.type.ts +21 -0
  341. package/templates/mobile/rn/src/core/components/ui/tag-input/index.ts +2 -0
  342. package/templates/mobile/rn/src/core/components/ui/tag-input/tag-input.tsx +146 -0
  343. package/templates/mobile/rn/src/core/components/ui/tag-input/tag-input.type.ts +22 -0
  344. package/templates/mobile/rn/src/core/components/ui/text-area/index.ts +2 -0
  345. package/templates/mobile/rn/src/core/components/ui/text-area/text-area.tsx +90 -0
  346. package/templates/mobile/rn/src/core/components/ui/text-area/text-area.type.ts +20 -0
  347. package/templates/mobile/rn/src/core/components/ui/text-field/index.ts +2 -0
  348. package/templates/mobile/rn/src/core/components/ui/text-field/text-field.tsx +116 -0
  349. package/templates/mobile/rn/src/core/components/ui/text-field/text-field.type.ts +21 -0
  350. package/templates/mobile/rn/src/core/components/ui/toggle/index.ts +2 -0
  351. package/templates/mobile/rn/src/core/components/ui/toggle/toggle.tsx +110 -0
  352. package/templates/mobile/rn/src/core/components/ui/toggle/toggle.type.ts +19 -0
  353. package/templates/mobile/rn/src/core/constants/external-urls.constant.ts +5 -0
  354. package/templates/mobile/rn/src/core/constants/hard-data.constant.ts +0 -0
  355. package/templates/mobile/rn/src/core/constants/index.ts +2 -0
  356. package/templates/mobile/rn/src/core/constants/type.constant.ts +3 -0
  357. package/templates/mobile/rn/src/core/context/index.ts +1 -0
  358. package/templates/mobile/rn/src/core/context/shared-transition-context.tsx +35 -0
  359. package/templates/mobile/rn/src/core/hook/index.ts +8 -0
  360. package/templates/mobile/rn/src/core/hook/useActiveRouteName.ts +63 -0
  361. package/templates/mobile/rn/src/core/hook/useAppNavigation.tsx +7 -0
  362. package/templates/mobile/rn/src/core/hook/useBottomInset.tsx +26 -0
  363. package/templates/mobile/rn/src/core/hook/useDebounce.tsx +16 -0
  364. package/templates/mobile/rn/src/core/hook/useEndReached.tsx +46 -0
  365. package/templates/mobile/rn/src/core/hook/useManualRefetch.ts +56 -0
  366. package/templates/mobile/rn/src/core/hook/useNetworkStatus.ts +68 -0
  367. package/templates/mobile/rn/src/core/hook/useTimeout.tsx +30 -0
  368. package/templates/mobile/rn/src/core/index.ts +7 -0
  369. package/templates/mobile/rn/src/core/services/api.service.ts +230 -0
  370. package/templates/mobile/rn/src/core/services/device-id.service.ts +23 -0
  371. package/templates/mobile/rn/src/core/services/index.ts +3 -0
  372. package/templates/mobile/rn/src/core/services/session-end.bridge.ts +26 -0
  373. package/templates/mobile/rn/src/core/theme/dark.theme.ts +10 -0
  374. package/templates/mobile/rn/src/core/theme/index.ts +5 -0
  375. package/templates/mobile/rn/src/core/theme/light.theme.ts +44 -0
  376. package/templates/mobile/rn/src/core/theme/theme-context.tsx +82 -0
  377. package/templates/mobile/rn/src/core/theme/theme.types.ts +26 -0
  378. package/templates/mobile/rn/src/core/theme/use-themed-styles.ts +25 -0
  379. package/templates/mobile/rn/src/core/utils/color.util.tsx +198 -0
  380. package/templates/mobile/rn/src/core/utils/date.util.ts +97 -0
  381. package/templates/mobile/rn/src/core/utils/device-locale.util.ts +22 -0
  382. package/templates/mobile/rn/src/core/utils/emitter/index.ts +161 -0
  383. package/templates/mobile/rn/src/core/utils/emitter/type.ts +40 -0
  384. package/templates/mobile/rn/src/core/utils/enum.util.tsx +15 -0
  385. package/templates/mobile/rn/src/core/utils/font.util.tsx +42 -0
  386. package/templates/mobile/rn/src/core/utils/func.util.ts +48 -0
  387. package/templates/mobile/rn/src/core/utils/greeting.util.ts +20 -0
  388. package/templates/mobile/rn/src/core/utils/image-format.util.ts +117 -0
  389. package/templates/mobile/rn/src/core/utils/image-picker.util.ts +84 -0
  390. package/templates/mobile/rn/src/core/utils/index.ts +18 -0
  391. package/templates/mobile/rn/src/core/utils/linking.util.ts +16 -0
  392. package/templates/mobile/rn/src/core/utils/navigation.util.tsx +100 -0
  393. package/templates/mobile/rn/src/core/utils/number-format.ts +28 -0
  394. package/templates/mobile/rn/src/core/utils/query-client.util.ts +35 -0
  395. package/templates/mobile/rn/src/core/utils/query-persister.util.ts +36 -0
  396. package/templates/mobile/rn/src/core/utils/schema.util.tsx +2713 -0
  397. package/templates/mobile/rn/src/core/utils/size.util.tsx +74 -0
  398. package/templates/mobile/rn/src/core/utils/storage.util.tsx +53 -0
  399. package/templates/mobile/rn/src/core/utils/toast.util.tsx +151 -0
  400. package/templates/mobile/rn/src/core/utils/translator.util.tsx +23 -0
  401. package/templates/mobile/rn/src/core/utils/typography.util.tsx +69 -0
  402. package/templates/mobile/rn/src/core/utils/validate.util.tsx +13 -0
  403. package/templates/mobile/rn/src/declarations.d.ts +54 -0
  404. package/templates/mobile/rn/src/modules/home/home.screen.tsx +33 -0
  405. package/templates/mobile/rn/src/modules/profile/profile.screen.tsx +29 -0
  406. package/templates/mobile/rn/src/scripts/link-fonts.js +60 -0
  407. package/templates/mobile/rn/src/scripts/sync-images.js +56 -0
  408. package/templates/mobile/rn/src/scripts/sync-svgs.js +50 -0
  409. package/templates/mobile/rn/src/types/models.d.ts +24 -0
  410. package/templates/mobile/rn/tsconfig.json +21 -0
  411. package/templates/shared/src/api-endpoints.ts +8 -0
  412. package/templates/shared/src/enums.ts +34 -0
  413. package/templates/shared/src/external-urls.ts +5 -0
  414. package/templates/shared/src/index.ts +6 -3
@@ -0,0 +1,22 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface SegmentItem {
5
+ /** Unique key. */
6
+ key: string;
7
+ /** Visible label. */
8
+ label: string;
9
+ /** Optional leading element (e.g. a flag/emoji icon). */
10
+ leading?: ReactNode;
11
+ }
12
+
13
+ export interface SegmentedControlProps {
14
+ /** Segments. */
15
+ items: SegmentItem[];
16
+ /** Currently selected key. */
17
+ selectedKey: string;
18
+ /** Fired with the next key on press. */
19
+ onChange: (key: string) => void;
20
+ /** Override the container style. */
21
+ style?: StyleProp<ViewStyle>;
22
+ }
@@ -0,0 +1,2 @@
1
+ export { default as Skeleton } from './skeleton';
2
+ export * from './skeleton.type';
@@ -0,0 +1,106 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { LayoutChangeEvent, StyleSheet, View } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+ import Animated, {
5
+ Easing,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withRepeat,
9
+ withTiming,
10
+ } from 'react-native-reanimated';
11
+ import { ThemeColors, useTheme, useThemedStyles } from '@src/core/theme';
12
+ import { SkeletonProps } from './skeleton.type';
13
+
14
+ // One sweep of the highlight band, edge to edge. Slower than a pulse so the
15
+ // motion reads as a deliberate "loading" sheen rather than a flicker.
16
+ const SWEEP_DURATION_MS = 1100;
17
+ // The moving band is ~60% of the block width, so a soft highlight crosses
18
+ // rather than a hard line.
19
+ const BAND_RATIO = 0.6;
20
+
21
+ const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
22
+
23
+ /**
24
+ * Loading placeholder block with a horizontal shimmer sweep.
25
+ *
26
+ * A dim base fills the box; a soft highlight band (LinearGradient) sweeps left
27
+ * → right on a loop on the UI thread (reanimated), giving the "professional"
28
+ * sheen instead of the old whole-block opacity pulse. The box clips the band
29
+ * (`overflow: hidden`) and measures its own width so the sweep distance is
30
+ * exact at any size.
31
+ */
32
+ const Skeleton: React.FC<SkeletonProps> = ({
33
+ width = '100%',
34
+ height = 16,
35
+ borderRadius = 8,
36
+ style,
37
+ }) => {
38
+ const styles = useThemedStyles(makeStyles);
39
+ const { colors } = useTheme();
40
+
41
+ // Measured pixel width drives the sweep distance (width can be a %/'100%').
42
+ const [boxWidth, setBoxWidth] = useState(0);
43
+ const progress = useSharedValue(0);
44
+
45
+ useEffect(() => {
46
+ if (boxWidth === 0) return;
47
+ progress.value = 0;
48
+ progress.value = withRepeat(
49
+ withTiming(1, { duration: SWEEP_DURATION_MS, easing: Easing.linear }),
50
+ -1, // infinite
51
+ false, // restart from the left each loop (no reverse)
52
+ );
53
+ }, [boxWidth, progress]);
54
+
55
+ const bandWidth = boxWidth * BAND_RATIO;
56
+
57
+ const bandStyle = useAnimatedStyle(() => ({
58
+ // Travel from fully off the left edge to fully off the right edge.
59
+ transform: [
60
+ {
61
+ translateX:
62
+ -bandWidth + progress.value * (boxWidth + bandWidth),
63
+ },
64
+ ],
65
+ }));
66
+
67
+ const onLayout = (e: LayoutChangeEvent) => {
68
+ const w = e.nativeEvent.layout.width;
69
+ if (w !== boxWidth) setBoxWidth(w);
70
+ };
71
+
72
+ return (
73
+ <View
74
+ onLayout={onLayout}
75
+ style={[styles.base, { width, height, borderRadius }, style]}
76
+ >
77
+ {boxWidth > 0 ? (
78
+ <AnimatedGradient
79
+ colors={[colors.transparent, colors.rgbaFFFFFF_20, colors.transparent]}
80
+ start={{ x: 0, y: 0.5 }}
81
+ end={{ x: 1, y: 0.5 }}
82
+ style={[styles.band, { width: bandWidth }, bandStyle]}
83
+ />
84
+ ) : null}
85
+ </View>
86
+ );
87
+ };
88
+
89
+ export default Skeleton;
90
+
91
+ const makeStyles = (c: ThemeColors) =>
92
+ StyleSheet.create({
93
+ base: {
94
+ backgroundColor: c.bg_elevation_level_2_normal, // #292929 dim block
95
+ overflow: 'hidden',
96
+ },
97
+ // Full-height band pinned to the left edge; its `width` is set inline from
98
+ // the measured box and it sweeps across via an animated translateX (so it
99
+ // must NOT pin `right`, or the width would be ignored).
100
+ band: {
101
+ position: 'absolute',
102
+ top: 0,
103
+ bottom: 0,
104
+ left: 0,
105
+ },
106
+ });
@@ -0,0 +1,8 @@
1
+ import { DimensionValue, StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export interface SkeletonProps {
4
+ width?: DimensionValue;
5
+ height?: DimensionValue;
6
+ borderRadius?: number;
7
+ style?: StyleProp<ViewStyle>;
8
+ }
@@ -0,0 +1 @@
1
+ export { default as SuccessState } from './success-state'
@@ -0,0 +1,68 @@
1
+ import { IconCheck } from '@src/assets/svgs'
2
+ import { Colors, Font, fontSize, horizontalScale, Spacing } from '@src/core/utils'
3
+ import React from 'react'
4
+ import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
5
+ import { SvgProps } from 'react-native-svg'
6
+
7
+ const BADGE_SIZE = horizontalScale(64)
8
+ const ICON_SIZE = horizontalScale(28)
9
+
10
+ interface SuccessStateProps {
11
+ title: string
12
+ subtitle?: string
13
+ /** Badge icon. @default IconCheck */
14
+ Icon?: React.FC<SvgProps>
15
+ style?: StyleProp<ViewStyle>
16
+ }
17
+
18
+ /**
19
+ * Design-system confirmation block (Life-Master-Design success screens).
20
+ *
21
+ * Centered success badge + title + optional subtitle. Used by the
22
+ * "reset link sent" and "password reset success" screens.
23
+ *
24
+ * @example
25
+ * <SuccessState title="Mot de passe mis à jour" subtitle="Connectez-vous…" />
26
+ */
27
+ const SuccessState = ({ title, subtitle, Icon = IconCheck, style }: SuccessStateProps) => {
28
+ return (
29
+ <View style={[styles.container, style]}>
30
+ <View style={styles.badge}>
31
+ <Icon width={ICON_SIZE} height={ICON_SIZE} />
32
+ </View>
33
+ <Text style={styles.title}>{title}</Text>
34
+ {subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
35
+ </View>
36
+ )
37
+ }
38
+
39
+ export default SuccessState
40
+
41
+ const styles = StyleSheet.create({
42
+ container: {
43
+ alignItems: 'center',
44
+ gap: Spacing.spacing_base,
45
+ },
46
+ badge: {
47
+ width: BADGE_SIZE,
48
+ height: BADGE_SIZE,
49
+ borderRadius: BADGE_SIZE,
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ backgroundColor: Colors.fg_success_normal,
53
+ },
54
+ title: {
55
+ fontFamily: Font.dmSansSemiBold,
56
+ fontSize: fontSize(24),
57
+ lineHeight: 32,
58
+ textAlign: 'center',
59
+ color: Colors.fg_neutral_on_background,
60
+ },
61
+ subtitle: {
62
+ fontFamily: Font.dmSansRegular,
63
+ fontSize: fontSize(14),
64
+ lineHeight: 20,
65
+ textAlign: 'center',
66
+ color: Colors.fg_neutral_faded,
67
+ },
68
+ })
@@ -0,0 +1,2 @@
1
+ export { default as Tabs } from './tabs';
2
+ export * from './tabs.type';
@@ -0,0 +1,273 @@
1
+ import { Font, fontSize, Radius, Spacing, Stroke } from '@src/core/utils';
2
+ import { ThemeColors, useTheme, useThemedStyles } from '@src/core/theme';
3
+ import React, { useCallback, useEffect, useRef } from 'react';
4
+ import {
5
+ LayoutChangeEvent,
6
+ ScrollView,
7
+ StyleSheet,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import Animated, {
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withTiming,
15
+ } from 'react-native-reanimated';
16
+ import { TabItem, TabsProps } from './tabs.type';
17
+
18
+ // One timing for both the underline slide and the label scale, so the
19
+ // indicator and the text settle together.
20
+ const SLIDE_DURATION = 240;
21
+ // Active label nudges up a touch — subtle; the row height stays put because the
22
+ // inactive label already reserves the line-height.
23
+ const LABEL_SCALE_ACTIVE = 1.06;
24
+
25
+ /** Per-tab measured geometry, used to place the sliding underline. */
26
+ type TabLayout = { x: number; width: number };
27
+
28
+ type Styles = ReturnType<typeof makeStyles>;
29
+
30
+ /**
31
+ * One tab cell. Measures itself (`onLayout`) so the parent can drive the
32
+ * sliding underline, and scales its label up while active. Memoized so a tab
33
+ * switch only re-renders the two tabs whose active state actually flips.
34
+ */
35
+ const Tab = React.memo(function Tab({
36
+ item,
37
+ isActive,
38
+ onPress,
39
+ onMeasure,
40
+ styles,
41
+ }: {
42
+ item: TabItem;
43
+ isActive: boolean;
44
+ onPress: (key: string) => void;
45
+ onMeasure: (key: string, layout: TabLayout) => void;
46
+ styles: Styles;
47
+ }) {
48
+ const scale = useSharedValue(isActive ? LABEL_SCALE_ACTIVE : 1);
49
+
50
+ useEffect(() => {
51
+ scale.value = withTiming(isActive ? LABEL_SCALE_ACTIVE : 1, {
52
+ duration: SLIDE_DURATION,
53
+ });
54
+ }, [isActive, scale]);
55
+
56
+ const labelStyle = useAnimatedStyle(() => ({
57
+ transform: [{ scale: scale.value }],
58
+ }));
59
+
60
+ const handleLayout = useCallback(
61
+ (e: LayoutChangeEvent) => {
62
+ const { x, width } = e.nativeEvent.layout;
63
+ onMeasure(item.key, { x, width });
64
+ },
65
+ [item.key, onMeasure],
66
+ );
67
+
68
+ const handlePress = useCallback(() => onPress(item.key), [item.key, onPress]);
69
+
70
+ return (
71
+ <TouchableOpacity
72
+ accessibilityRole="tab"
73
+ accessibilityState={{ selected: isActive }}
74
+ activeOpacity={0.7}
75
+ onPress={handlePress}
76
+ onLayout={handleLayout}
77
+ style={styles.tab}
78
+ >
79
+ <Animated.Text
80
+ style={[styles.label, isActive && styles.labelActive, labelStyle]}
81
+ >
82
+ {item.label}
83
+ </Animated.Text>
84
+ </TouchableOpacity>
85
+ );
86
+ });
87
+
88
+ /**
89
+ * Design-system underline tabs (Life-Master-Design "Horizontal tabs").
90
+ *
91
+ * The active tab keeps the same look as before — a 2px primary underline + a
92
+ * highlighted label — but the underline now *slides* between tabs (reanimated)
93
+ * and the active label scales up a touch, so switching reads as continuous
94
+ * motion instead of a hard swap. The whole bar keeps its 1px bottom divider.
95
+ *
96
+ * @example
97
+ * <Tabs items={[{key:'all',label:'Tout'}]} activeKey={key} onChange={setKey} />
98
+ */
99
+ const Tabs = ({ items, activeKey, onChange, scrollable = true, style }: TabsProps) => {
100
+ const styles = useThemedStyles(makeStyles);
101
+ const { colors } = useTheme();
102
+
103
+ // Measured geometry per tab key — the underline reads from here. A ref, so a
104
+ // measurement never triggers a render; the underline is repositioned
105
+ // imperatively on the UI thread instead.
106
+ const layouts = useRef<Record<string, TabLayout>>({});
107
+
108
+ const underlineX = useSharedValue(0);
109
+ const underlineW = useSharedValue(0);
110
+ // Skip the first animation so the underline appears already under the active
111
+ // tab instead of flying in from x=0 on mount.
112
+ const placed = useRef(false);
113
+
114
+ // The horizontal scroller + its measured viewport width, so a tap on a tab
115
+ // near either edge can recenter that tab. Width is needed to compute the
116
+ // centering offset; the ref drives the imperative scroll.
117
+ const scrollRef = useRef<ScrollView>(null);
118
+ const viewportW = useRef(0);
119
+
120
+ // Scroll so the tapped tab sits centered in the viewport, clamped at both
121
+ // ends so we never overscroll past the content edges.
122
+ const centerTab = useCallback((key: string) => {
123
+ const l = layouts.current[key];
124
+ const vw = viewportW.current;
125
+ if (!l || !vw) return;
126
+ const target = l.x + l.width / 2 - vw / 2;
127
+ scrollRef.current?.scrollTo({ x: Math.max(0, target), animated: true });
128
+ }, []);
129
+
130
+ const placeUnderline = useCallback(
131
+ (key: string, animated: boolean) => {
132
+ const l = layouts.current[key];
133
+ if (!l) return;
134
+ if (animated) {
135
+ underlineX.value = withTiming(l.x, { duration: SLIDE_DURATION });
136
+ underlineW.value = withTiming(l.width, { duration: SLIDE_DURATION });
137
+ } else {
138
+ underlineX.value = l.x;
139
+ underlineW.value = l.width;
140
+ }
141
+ },
142
+ [underlineX, underlineW],
143
+ );
144
+
145
+ const onMeasure = useCallback(
146
+ (key: string, layout: TabLayout) => {
147
+ const prev = layouts.current[key];
148
+ if (prev && prev.x === layout.x && prev.width === layout.width) return;
149
+ layouts.current[key] = layout;
150
+ // First time the active tab reports its box, snap the underline under it.
151
+ if (key === activeKey && !placed.current) {
152
+ placeUnderline(key, false);
153
+ placed.current = true;
154
+ }
155
+ },
156
+ [activeKey, placeUnderline],
157
+ );
158
+
159
+ // Slide whenever the active tab changes. If the active tab has already been
160
+ // measured, `placed` is true → animate; otherwise the measure callback snaps.
161
+ // Also recenter the now-active tab so edge tabs (and external swipes that
162
+ // move the active key) pull themselves into the middle of the scroller.
163
+ useEffect(() => {
164
+ placeUnderline(activeKey, placed.current);
165
+ centerTab(activeKey);
166
+ }, [activeKey, placeUnderline, centerTab]);
167
+
168
+ const underlineStyle = useAnimatedStyle(() => ({
169
+ transform: [{ translateX: underlineX.value }],
170
+ width: underlineW.value,
171
+ }));
172
+
173
+ const row = items.map((item) => (
174
+ <Tab
175
+ key={item.key}
176
+ item={item}
177
+ isActive={item.key === activeKey}
178
+ onPress={onChange}
179
+ onMeasure={onMeasure}
180
+ styles={styles}
181
+ />
182
+ ));
183
+
184
+ // Absolutely positioned over the bar; left + width animate to the active tab.
185
+ const underline = (
186
+ <Animated.View
187
+ pointerEvents="none"
188
+ style={[
189
+ styles.underline,
190
+ { backgroundColor: colors.bd_primary_normal },
191
+ underlineStyle,
192
+ ]}
193
+ />
194
+ );
195
+
196
+ if (scrollable) {
197
+ return (
198
+ <View style={styles.content}>
199
+ <ScrollView
200
+ ref={scrollRef}
201
+ horizontal
202
+ showsHorizontalScrollIndicator={false}
203
+ onLayout={(e) => {
204
+ viewportW.current = e.nativeEvent.layout.width;
205
+ }}
206
+ style={style}
207
+ contentContainerStyle={styles.bar}
208
+ >
209
+ {row}
210
+ {underline}
211
+ </ScrollView>
212
+ </View>
213
+ );
214
+ }
215
+
216
+ return (
217
+ <View style={[styles.bar, styles.barStatic, style]}>
218
+ {row}
219
+ {underline}
220
+ </View>
221
+ );
222
+ };
223
+
224
+ export default Tabs;
225
+
226
+ const makeStyles = (c: ThemeColors) =>
227
+ StyleSheet.create({
228
+ bar: {
229
+ flexDirection: 'row',
230
+ alignItems: 'center',
231
+ gap: Spacing.spacing_sm, // 8
232
+ // Anchor the absolute sliding underline to the bar's bottom edge.
233
+ position: 'relative',
234
+ paddingBottom: Stroke.stroke_m, // 2 — room for the underline
235
+ },
236
+ barStatic: {
237
+ alignSelf: 'stretch',
238
+ },
239
+ content: {
240
+ borderBottomWidth: Stroke.stroke_m, // 1
241
+ borderBottomColor: c.bd_neutral_faded, // #525252
242
+ },
243
+ tab: {
244
+ minHeight: 46,
245
+ alignItems: 'center',
246
+ justifyContent: 'center',
247
+ paddingHorizontal: Spacing.spacing_base, // 16
248
+ paddingTop: Spacing.spacing_sm, // 8
249
+ paddingBottom: Spacing.spacing_m_nudge, // 10
250
+ borderTopLeftRadius: Radius.radius_md, // 8
251
+ borderTopRightRadius: Radius.radius_md,
252
+ },
253
+ // Slides between tabs; left (translateX) + width are animated inline.
254
+ underline: {
255
+ position: 'absolute',
256
+ left: 0,
257
+ bottom: 0,
258
+ height: Stroke.stroke_m, // 2
259
+ borderRadius: Stroke.stroke_m,
260
+ },
261
+ label: {
262
+ fontFamily: Font.dmSansMedium,
263
+ fontSize: fontSize(14),
264
+ lineHeight: 20,
265
+ letterSpacing: 0.25,
266
+ color: c.fg_neutral_faded, // #a8a8a8
267
+ paddingHorizontal: Spacing.spacing_s_nudge, // 6
268
+ paddingVertical: Spacing.spacing_2xs, // 2
269
+ },
270
+ labelActive: {
271
+ color: c.fg_primary_faded, // #ece9fe
272
+ },
273
+ });
@@ -0,0 +1,21 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export interface TabItem {
4
+ /** Unique key. */
5
+ key: string;
6
+ /** Visible label. */
7
+ label: string;
8
+ }
9
+
10
+ export interface TabsProps {
11
+ /** Tab items. */
12
+ items: TabItem[];
13
+ /** Currently active tab key. */
14
+ activeKey: string;
15
+ /** Fired with the next key when a tab is pressed. */
16
+ onChange: (key: string) => void;
17
+ /** Scroll horizontally when tabs overflow. @default true */
18
+ scrollable?: boolean;
19
+ /** Override the container style. */
20
+ style?: StyleProp<ViewStyle>;
21
+ }
@@ -0,0 +1,2 @@
1
+ export { default as TagInput } from './tag-input';
2
+ export * from './tag-input.type';
@@ -0,0 +1,146 @@
1
+ import { IconClose } from '@src/assets/svgs';
2
+ import { ThemeColors, useTheme, useThemedStyles } from '@src/core/theme';
3
+ import { Font, fontSize, horizontalScale, Radius, Spacing, Stroke } from '@src/core/utils';
4
+ import React, { useState } from 'react';
5
+ import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
6
+ import FieldFrame from '../field/field-frame';
7
+ import { FIELD_BORDER_WIDTH, resolveFieldBorderColor } from '../field/field.shared';
8
+ import { TagInputProps } from './tag-input.type';
9
+
10
+ /**
11
+ * Design-system tag / badge input (Life-Master-Design "Badge" input field).
12
+ *
13
+ * Removable chips + a free-text input. Submitting the input adds a tag;
14
+ * backspace on an empty input removes the last tag. Shares the field frame
15
+ * + border-state logic with {@link TextField}.
16
+ *
17
+ * @example
18
+ * <TagInput label="People" tags={tags} onChangeTags={setTags} placeholder="Add…" />
19
+ */
20
+ const TagInput = (props: TagInputProps) => {
21
+ const {
22
+ label,
23
+ message,
24
+ tags,
25
+ onChangeTags,
26
+ placeholder,
27
+ error = false,
28
+ disabled = false,
29
+ containerStyle,
30
+ inputStyle,
31
+ } = props;
32
+
33
+ const { colors } = useTheme();
34
+ const styles = useThemedStyles(makeStyles);
35
+
36
+ const [text, setText] = useState('');
37
+ const [focused, setFocused] = useState(false);
38
+ const borderColor = resolveFieldBorderColor(colors, { focused, error, disabled });
39
+
40
+ const addTag = () => {
41
+ const value = text.trim();
42
+ if (value) {
43
+ onChangeTags([...tags, value]);
44
+ setText('');
45
+ }
46
+ };
47
+
48
+ const removeTag = (index: number) => {
49
+ onChangeTags(tags.filter((_, i) => i !== index));
50
+ };
51
+
52
+ const handleKeyPress = (key: string) => {
53
+ if (key === 'Backspace' && text.length === 0 && tags.length > 0) {
54
+ removeTag(tags.length - 1);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <FieldFrame label={label} message={message} error={error} disabled={disabled} style={containerStyle}>
60
+ <View
61
+ style={[
62
+ styles.box,
63
+ { borderColor, backgroundColor: disabled ? colors.bg_disable : colors.bg_input },
64
+ ]}
65
+ >
66
+ {tags.map((tag, i) => (
67
+ <View key={`${tag}-${i}`} style={styles.chip}>
68
+ <Text style={styles.chipText}>{tag}</Text>
69
+ <TouchableOpacity disabled={disabled} onPress={() => removeTag(i)} hitSlop={6}>
70
+ <IconClose
71
+ width={horizontalScale(12)}
72
+ height={horizontalScale(12)}
73
+ color={colors.fg_neutral_faded}
74
+ />
75
+ </TouchableOpacity>
76
+ </View>
77
+ ))}
78
+
79
+ <TextInput
80
+ value={text}
81
+ editable={!disabled}
82
+ placeholder={tags.length === 0 ? placeholder : undefined}
83
+ placeholderTextColor={colors.fg_neutral_faded}
84
+ onChangeText={setText}
85
+ onSubmitEditing={addTag}
86
+ onKeyPress={(e) => {
87
+ // Android may report a null/undefined key — guard before handling.
88
+ if (e.nativeEvent?.key) {
89
+ handleKeyPress(e.nativeEvent.key);
90
+ }
91
+ }}
92
+ onFocus={() => setFocused(true)}
93
+ onBlur={() => {
94
+ setFocused(false);
95
+ addTag();
96
+ }}
97
+ blurOnSubmit={false}
98
+ style={[styles.input, inputStyle]}
99
+ />
100
+ </View>
101
+ </FieldFrame>
102
+ );
103
+ };
104
+
105
+ export default TagInput;
106
+
107
+ const makeStyles = (c: ThemeColors) =>
108
+ StyleSheet.create({
109
+ box: {
110
+ flexDirection: 'row',
111
+ flexWrap: 'wrap',
112
+ alignItems: 'center',
113
+ alignSelf: 'stretch',
114
+ gap: Spacing.spacing_xs, // 4
115
+ borderRadius: Radius.radius_lg, // 12
116
+ borderWidth: FIELD_BORDER_WIDTH, // 1
117
+ paddingHorizontal: Spacing.spacing_md, // 12
118
+ paddingVertical: Spacing.spacing_s_nudge, // 6
119
+ },
120
+ chip: {
121
+ flexDirection: 'row',
122
+ alignItems: 'center',
123
+ gap: Spacing.spacing_xs, // 4
124
+ paddingHorizontal: Spacing.spacing_sm, // 8
125
+ paddingVertical: Spacing.spacing_2xs, // 2
126
+ borderRadius: Radius.radius_sm, // 4
127
+ borderWidth: Stroke.stroke_xs, // 0.5
128
+ borderColor: c.bd_neutral_faded,
129
+ backgroundColor: c.bg_elevation_level_2_normal, // #292929
130
+ },
131
+ chipText: {
132
+ fontFamily: Font.dmSansMedium,
133
+ fontSize: fontSize(12),
134
+ lineHeight: 18,
135
+ color: c.fg_neutral_normal,
136
+ },
137
+ input: {
138
+ flexGrow: 1,
139
+ minWidth: horizontalScale(60),
140
+ paddingVertical: Spacing.spacing_xs, // 4
141
+ fontFamily: Font.dmSansRegular,
142
+ fontSize: fontSize(16),
143
+ lineHeight: 24,
144
+ color: c.fg_neutral_normal,
145
+ },
146
+ });
@@ -0,0 +1,22 @@
1
+ import { StyleProp, TextStyle, ViewStyle } from 'react-native';
2
+
3
+ export interface TagInputProps {
4
+ /** Top label. */
5
+ label?: string;
6
+ /** Bottom helper / error message. */
7
+ message?: string;
8
+ /** Current list of tags (controlled). */
9
+ tags: string[];
10
+ /** Fired with the next list when a tag is added or removed. */
11
+ onChangeTags: (tags: string[]) => void;
12
+ /** Placeholder for the text input. */
13
+ placeholder?: string;
14
+ /** Error (validation) visual + red message. */
15
+ error?: boolean;
16
+ /** Disable input. */
17
+ disabled?: boolean;
18
+ /** Override the outer container style. */
19
+ containerStyle?: StyleProp<ViewStyle>;
20
+ /** Override the text input style. */
21
+ inputStyle?: StyleProp<TextStyle>;
22
+ };
@@ -0,0 +1,2 @@
1
+ export { default as TextArea } from './text-area';
2
+ export * from './text-area.type';