orn-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/README.md +107 -0
  2. package/bin/orn-ui.mjs +228 -0
  3. package/lib/commonjs/atoms/Avatar.js +42 -0
  4. package/lib/commonjs/atoms/Avatar.js.map +1 -0
  5. package/lib/commonjs/atoms/Badge.js +82 -0
  6. package/lib/commonjs/atoms/Badge.js.map +1 -0
  7. package/lib/commonjs/atoms/Button.js +222 -0
  8. package/lib/commonjs/atoms/Button.js.map +1 -0
  9. package/lib/commonjs/atoms/Card.js +33 -0
  10. package/lib/commonjs/atoms/Card.js.map +1 -0
  11. package/lib/commonjs/atoms/Checkbox.js +107 -0
  12. package/lib/commonjs/atoms/Checkbox.js.map +1 -0
  13. package/lib/commonjs/atoms/Divider.js +27 -0
  14. package/lib/commonjs/atoms/Divider.js.map +1 -0
  15. package/lib/commonjs/atoms/EmptyState.js +69 -0
  16. package/lib/commonjs/atoms/EmptyState.js.map +1 -0
  17. package/lib/commonjs/atoms/Fab.js +62 -0
  18. package/lib/commonjs/atoms/Fab.js.map +1 -0
  19. package/lib/commonjs/atoms/IconButton.js +60 -0
  20. package/lib/commonjs/atoms/IconButton.js.map +1 -0
  21. package/lib/commonjs/atoms/Image.js +113 -0
  22. package/lib/commonjs/atoms/Image.js.map +1 -0
  23. package/lib/commonjs/atoms/Input.js +173 -0
  24. package/lib/commonjs/atoms/Input.js.map +1 -0
  25. package/lib/commonjs/atoms/KeyValueRow.js +60 -0
  26. package/lib/commonjs/atoms/KeyValueRow.js.map +1 -0
  27. package/lib/commonjs/atoms/PressableScale.js +60 -0
  28. package/lib/commonjs/atoms/PressableScale.js.map +1 -0
  29. package/lib/commonjs/atoms/Spinner.js +156 -0
  30. package/lib/commonjs/atoms/Spinner.js.map +1 -0
  31. package/lib/commonjs/atoms/Text.js +63 -0
  32. package/lib/commonjs/atoms/Text.js.map +1 -0
  33. package/lib/commonjs/atoms/index.js +135 -0
  34. package/lib/commonjs/atoms/index.js.map +1 -0
  35. package/lib/commonjs/icons/Icon.js +40 -0
  36. package/lib/commonjs/icons/Icon.js.map +1 -0
  37. package/lib/commonjs/icons/defaultIcons.js +386 -0
  38. package/lib/commonjs/icons/defaultIcons.js.map +1 -0
  39. package/lib/commonjs/icons/index.js +20 -0
  40. package/lib/commonjs/icons/index.js.map +1 -0
  41. package/lib/commonjs/icons/types.js +6 -0
  42. package/lib/commonjs/icons/types.js.map +1 -0
  43. package/lib/commonjs/index.js +61 -0
  44. package/lib/commonjs/index.js.map +1 -0
  45. package/lib/commonjs/molecules/AvatarHeader.js +87 -0
  46. package/lib/commonjs/molecules/AvatarHeader.js.map +1 -0
  47. package/lib/commonjs/molecules/FormActions.js +47 -0
  48. package/lib/commonjs/molecules/FormActions.js.map +1 -0
  49. package/lib/commonjs/molecules/InfoRow.js +83 -0
  50. package/lib/commonjs/molecules/InfoRow.js.map +1 -0
  51. package/lib/commonjs/molecules/OptionCard.js +98 -0
  52. package/lib/commonjs/molecules/OptionCard.js.map +1 -0
  53. package/lib/commonjs/molecules/Stepper.js +104 -0
  54. package/lib/commonjs/molecules/Stepper.js.map +1 -0
  55. package/lib/commonjs/molecules/Steps.js +220 -0
  56. package/lib/commonjs/molecules/Steps.js.map +1 -0
  57. package/lib/commonjs/molecules/ThemeToggle.js +91 -0
  58. package/lib/commonjs/molecules/ThemeToggle.js.map +1 -0
  59. package/lib/commonjs/molecules/index.js +55 -0
  60. package/lib/commonjs/molecules/index.js.map +1 -0
  61. package/lib/commonjs/organisms/Alert.js +223 -0
  62. package/lib/commonjs/organisms/Alert.js.map +1 -0
  63. package/lib/commonjs/organisms/AlertProvider.js +109 -0
  64. package/lib/commonjs/organisms/AlertProvider.js.map +1 -0
  65. package/lib/commonjs/organisms/BottomSheet.js +154 -0
  66. package/lib/commonjs/organisms/BottomSheet.js.map +1 -0
  67. package/lib/commonjs/organisms/DateField.js +147 -0
  68. package/lib/commonjs/organisms/DateField.js.map +1 -0
  69. package/lib/commonjs/organisms/DatePicker.js +253 -0
  70. package/lib/commonjs/organisms/DatePicker.js.map +1 -0
  71. package/lib/commonjs/organisms/List.js +143 -0
  72. package/lib/commonjs/organisms/List.js.map +1 -0
  73. package/lib/commonjs/organisms/Modal.js +286 -0
  74. package/lib/commonjs/organisms/Modal.js.map +1 -0
  75. package/lib/commonjs/organisms/Screen.js +75 -0
  76. package/lib/commonjs/organisms/Screen.js.map +1 -0
  77. package/lib/commonjs/organisms/SearchList.js +180 -0
  78. package/lib/commonjs/organisms/SearchList.js.map +1 -0
  79. package/lib/commonjs/organisms/Select.js +178 -0
  80. package/lib/commonjs/organisms/Select.js.map +1 -0
  81. package/lib/commonjs/organisms/Toast.js +157 -0
  82. package/lib/commonjs/organisms/Toast.js.map +1 -0
  83. package/lib/commonjs/organisms/ToastProvider.js +131 -0
  84. package/lib/commonjs/organisms/ToastProvider.js.map +1 -0
  85. package/lib/commonjs/organisms/Wizard.js +116 -0
  86. package/lib/commonjs/organisms/Wizard.js.map +1 -0
  87. package/lib/commonjs/organisms/index.js +127 -0
  88. package/lib/commonjs/organisms/index.js.map +1 -0
  89. package/lib/commonjs/package.json +1 -0
  90. package/lib/commonjs/theme/UIProvider.js +110 -0
  91. package/lib/commonjs/theme/UIProvider.js.map +1 -0
  92. package/lib/commonjs/theme/createStyles.js +42 -0
  93. package/lib/commonjs/theme/createStyles.js.map +1 -0
  94. package/lib/commonjs/theme/createTheme.js +50 -0
  95. package/lib/commonjs/theme/createTheme.js.map +1 -0
  96. package/lib/commonjs/theme/index.js +151 -0
  97. package/lib/commonjs/theme/index.js.map +1 -0
  98. package/lib/commonjs/theme/insets.js +15 -0
  99. package/lib/commonjs/theme/insets.js.map +1 -0
  100. package/lib/commonjs/theme/labels.js +18 -0
  101. package/lib/commonjs/theme/labels.js.map +1 -0
  102. package/lib/commonjs/theme/palettes.js +42 -0
  103. package/lib/commonjs/theme/palettes.js.map +1 -0
  104. package/lib/commonjs/theme/tokens.js +120 -0
  105. package/lib/commonjs/theme/tokens.js.map +1 -0
  106. package/lib/commonjs/theme/types.js +6 -0
  107. package/lib/commonjs/theme/types.js.map +1 -0
  108. package/lib/module/atoms/Avatar.js +37 -0
  109. package/lib/module/atoms/Avatar.js.map +1 -0
  110. package/lib/module/atoms/Badge.js +77 -0
  111. package/lib/module/atoms/Badge.js.map +1 -0
  112. package/lib/module/atoms/Button.js +217 -0
  113. package/lib/module/atoms/Button.js.map +1 -0
  114. package/lib/module/atoms/Card.js +28 -0
  115. package/lib/module/atoms/Card.js.map +1 -0
  116. package/lib/module/atoms/Checkbox.js +102 -0
  117. package/lib/module/atoms/Checkbox.js.map +1 -0
  118. package/lib/module/atoms/Divider.js +22 -0
  119. package/lib/module/atoms/Divider.js.map +1 -0
  120. package/lib/module/atoms/EmptyState.js +64 -0
  121. package/lib/module/atoms/EmptyState.js.map +1 -0
  122. package/lib/module/atoms/Fab.js +57 -0
  123. package/lib/module/atoms/Fab.js.map +1 -0
  124. package/lib/module/atoms/IconButton.js +55 -0
  125. package/lib/module/atoms/IconButton.js.map +1 -0
  126. package/lib/module/atoms/Image.js +108 -0
  127. package/lib/module/atoms/Image.js.map +1 -0
  128. package/lib/module/atoms/Input.js +168 -0
  129. package/lib/module/atoms/Input.js.map +1 -0
  130. package/lib/module/atoms/KeyValueRow.js +55 -0
  131. package/lib/module/atoms/KeyValueRow.js.map +1 -0
  132. package/lib/module/atoms/PressableScale.js +55 -0
  133. package/lib/module/atoms/PressableScale.js.map +1 -0
  134. package/lib/module/atoms/Spinner.js +151 -0
  135. package/lib/module/atoms/Spinner.js.map +1 -0
  136. package/lib/module/atoms/Text.js +58 -0
  137. package/lib/module/atoms/Text.js.map +1 -0
  138. package/lib/module/atoms/index.js +18 -0
  139. package/lib/module/atoms/index.js.map +1 -0
  140. package/lib/module/icons/Icon.js +35 -0
  141. package/lib/module/icons/Icon.js.map +1 -0
  142. package/lib/module/icons/defaultIcons.js +381 -0
  143. package/lib/module/icons/defaultIcons.js.map +1 -0
  144. package/lib/module/icons/index.js +5 -0
  145. package/lib/module/icons/index.js.map +1 -0
  146. package/lib/module/icons/types.js +4 -0
  147. package/lib/module/icons/types.js.map +1 -0
  148. package/lib/module/index.js +8 -0
  149. package/lib/module/index.js.map +1 -0
  150. package/lib/module/molecules/AvatarHeader.js +82 -0
  151. package/lib/module/molecules/AvatarHeader.js.map +1 -0
  152. package/lib/module/molecules/FormActions.js +42 -0
  153. package/lib/module/molecules/FormActions.js.map +1 -0
  154. package/lib/module/molecules/InfoRow.js +78 -0
  155. package/lib/module/molecules/InfoRow.js.map +1 -0
  156. package/lib/module/molecules/OptionCard.js +93 -0
  157. package/lib/module/molecules/OptionCard.js.map +1 -0
  158. package/lib/module/molecules/Stepper.js +99 -0
  159. package/lib/module/molecules/Stepper.js.map +1 -0
  160. package/lib/module/molecules/Steps.js +215 -0
  161. package/lib/module/molecules/Steps.js.map +1 -0
  162. package/lib/module/molecules/ThemeToggle.js +86 -0
  163. package/lib/module/molecules/ThemeToggle.js.map +1 -0
  164. package/lib/module/molecules/index.js +10 -0
  165. package/lib/module/molecules/index.js.map +1 -0
  166. package/lib/module/organisms/Alert.js +218 -0
  167. package/lib/module/organisms/Alert.js.map +1 -0
  168. package/lib/module/organisms/AlertProvider.js +103 -0
  169. package/lib/module/organisms/AlertProvider.js.map +1 -0
  170. package/lib/module/organisms/BottomSheet.js +149 -0
  171. package/lib/module/organisms/BottomSheet.js.map +1 -0
  172. package/lib/module/organisms/DateField.js +142 -0
  173. package/lib/module/organisms/DateField.js.map +1 -0
  174. package/lib/module/organisms/DatePicker.js +247 -0
  175. package/lib/module/organisms/DatePicker.js.map +1 -0
  176. package/lib/module/organisms/List.js +138 -0
  177. package/lib/module/organisms/List.js.map +1 -0
  178. package/lib/module/organisms/Modal.js +281 -0
  179. package/lib/module/organisms/Modal.js.map +1 -0
  180. package/lib/module/organisms/Screen.js +70 -0
  181. package/lib/module/organisms/Screen.js.map +1 -0
  182. package/lib/module/organisms/SearchList.js +175 -0
  183. package/lib/module/organisms/SearchList.js.map +1 -0
  184. package/lib/module/organisms/Select.js +173 -0
  185. package/lib/module/organisms/Select.js.map +1 -0
  186. package/lib/module/organisms/Toast.js +152 -0
  187. package/lib/module/organisms/Toast.js.map +1 -0
  188. package/lib/module/organisms/ToastProvider.js +125 -0
  189. package/lib/module/organisms/ToastProvider.js.map +1 -0
  190. package/lib/module/organisms/Wizard.js +111 -0
  191. package/lib/module/organisms/Wizard.js.map +1 -0
  192. package/lib/module/organisms/index.js +16 -0
  193. package/lib/module/organisms/index.js.map +1 -0
  194. package/lib/module/package.json +1 -0
  195. package/lib/module/theme/UIProvider.js +97 -0
  196. package/lib/module/theme/UIProvider.js.map +1 -0
  197. package/lib/module/theme/createStyles.js +38 -0
  198. package/lib/module/theme/createStyles.js.map +1 -0
  199. package/lib/module/theme/createTheme.js +45 -0
  200. package/lib/module/theme/createTheme.js.map +1 -0
  201. package/lib/module/theme/index.js +10 -0
  202. package/lib/module/theme/index.js.map +1 -0
  203. package/lib/module/theme/insets.js +11 -0
  204. package/lib/module/theme/insets.js.map +1 -0
  205. package/lib/module/theme/labels.js +14 -0
  206. package/lib/module/theme/labels.js.map +1 -0
  207. package/lib/module/theme/palettes.js +38 -0
  208. package/lib/module/theme/palettes.js.map +1 -0
  209. package/lib/module/theme/tokens.js +117 -0
  210. package/lib/module/theme/tokens.js.map +1 -0
  211. package/lib/module/theme/types.js +4 -0
  212. package/lib/module/theme/types.js.map +1 -0
  213. package/lib/typescript/atoms/Avatar.d.ts +15 -0
  214. package/lib/typescript/atoms/Avatar.d.ts.map +1 -0
  215. package/lib/typescript/atoms/Badge.d.ts +19 -0
  216. package/lib/typescript/atoms/Badge.d.ts.map +1 -0
  217. package/lib/typescript/atoms/Button.d.ts +30 -0
  218. package/lib/typescript/atoms/Button.d.ts.map +1 -0
  219. package/lib/typescript/atoms/Card.d.ts +8 -0
  220. package/lib/typescript/atoms/Card.d.ts.map +1 -0
  221. package/lib/typescript/atoms/Checkbox.d.ts +11 -0
  222. package/lib/typescript/atoms/Checkbox.d.ts.map +1 -0
  223. package/lib/typescript/atoms/Divider.d.ts +8 -0
  224. package/lib/typescript/atoms/Divider.d.ts.map +1 -0
  225. package/lib/typescript/atoms/EmptyState.d.ts +12 -0
  226. package/lib/typescript/atoms/EmptyState.d.ts.map +1 -0
  227. package/lib/typescript/atoms/Fab.d.ts +22 -0
  228. package/lib/typescript/atoms/Fab.d.ts.map +1 -0
  229. package/lib/typescript/atoms/IconButton.d.ts +23 -0
  230. package/lib/typescript/atoms/IconButton.d.ts.map +1 -0
  231. package/lib/typescript/atoms/Image.d.ts +45 -0
  232. package/lib/typescript/atoms/Image.d.ts.map +1 -0
  233. package/lib/typescript/atoms/Input.d.ts +24 -0
  234. package/lib/typescript/atoms/Input.d.ts.map +1 -0
  235. package/lib/typescript/atoms/KeyValueRow.d.ts +17 -0
  236. package/lib/typescript/atoms/KeyValueRow.d.ts.map +1 -0
  237. package/lib/typescript/atoms/PressableScale.d.ts +15 -0
  238. package/lib/typescript/atoms/PressableScale.d.ts.map +1 -0
  239. package/lib/typescript/atoms/Spinner.d.ts +20 -0
  240. package/lib/typescript/atoms/Spinner.d.ts.map +1 -0
  241. package/lib/typescript/atoms/Text.d.ts +13 -0
  242. package/lib/typescript/atoms/Text.d.ts.map +1 -0
  243. package/lib/typescript/atoms/index.d.ts +31 -0
  244. package/lib/typescript/atoms/index.d.ts.map +1 -0
  245. package/lib/typescript/icons/Icon.d.ts +18 -0
  246. package/lib/typescript/icons/Icon.d.ts.map +1 -0
  247. package/lib/typescript/icons/defaultIcons.d.ts +5 -0
  248. package/lib/typescript/icons/defaultIcons.d.ts.map +1 -0
  249. package/lib/typescript/icons/index.d.ts +5 -0
  250. package/lib/typescript/icons/index.d.ts.map +1 -0
  251. package/lib/typescript/icons/types.d.ts +10 -0
  252. package/lib/typescript/icons/types.d.ts.map +1 -0
  253. package/lib/typescript/index.d.ts +6 -0
  254. package/lib/typescript/index.d.ts.map +1 -0
  255. package/lib/typescript/molecules/AvatarHeader.d.ts +20 -0
  256. package/lib/typescript/molecules/AvatarHeader.d.ts.map +1 -0
  257. package/lib/typescript/molecules/FormActions.d.ts +21 -0
  258. package/lib/typescript/molecules/FormActions.d.ts.map +1 -0
  259. package/lib/typescript/molecules/InfoRow.d.ts +17 -0
  260. package/lib/typescript/molecules/InfoRow.d.ts.map +1 -0
  261. package/lib/typescript/molecules/OptionCard.d.ts +20 -0
  262. package/lib/typescript/molecules/OptionCard.d.ts.map +1 -0
  263. package/lib/typescript/molecules/Stepper.d.ts +15 -0
  264. package/lib/typescript/molecules/Stepper.d.ts.map +1 -0
  265. package/lib/typescript/molecules/Steps.d.ts +29 -0
  266. package/lib/typescript/molecules/Steps.d.ts.map +1 -0
  267. package/lib/typescript/molecules/ThemeToggle.d.ts +15 -0
  268. package/lib/typescript/molecules/ThemeToggle.d.ts.map +1 -0
  269. package/lib/typescript/molecules/index.d.ts +15 -0
  270. package/lib/typescript/molecules/index.d.ts.map +1 -0
  271. package/lib/typescript/organisms/Alert.d.ts +29 -0
  272. package/lib/typescript/organisms/Alert.d.ts.map +1 -0
  273. package/lib/typescript/organisms/AlertProvider.d.ts +32 -0
  274. package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -0
  275. package/lib/typescript/organisms/BottomSheet.d.ts +15 -0
  276. package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -0
  277. package/lib/typescript/organisms/DateField.d.ts +26 -0
  278. package/lib/typescript/organisms/DateField.d.ts.map +1 -0
  279. package/lib/typescript/organisms/DatePicker.d.ts +49 -0
  280. package/lib/typescript/organisms/DatePicker.d.ts.map +1 -0
  281. package/lib/typescript/organisms/List.d.ts +35 -0
  282. package/lib/typescript/organisms/List.d.ts.map +1 -0
  283. package/lib/typescript/organisms/Modal.d.ts +25 -0
  284. package/lib/typescript/organisms/Modal.d.ts.map +1 -0
  285. package/lib/typescript/organisms/Screen.d.ts +26 -0
  286. package/lib/typescript/organisms/Screen.d.ts.map +1 -0
  287. package/lib/typescript/organisms/SearchList.d.ts +40 -0
  288. package/lib/typescript/organisms/SearchList.d.ts.map +1 -0
  289. package/lib/typescript/organisms/Select.d.ts +25 -0
  290. package/lib/typescript/organisms/Select.d.ts.map +1 -0
  291. package/lib/typescript/organisms/Toast.d.ts +28 -0
  292. package/lib/typescript/organisms/Toast.d.ts.map +1 -0
  293. package/lib/typescript/organisms/ToastProvider.d.ts +36 -0
  294. package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -0
  295. package/lib/typescript/organisms/Wizard.d.ts +40 -0
  296. package/lib/typescript/organisms/Wizard.d.ts.map +1 -0
  297. package/lib/typescript/organisms/index.d.ts +27 -0
  298. package/lib/typescript/organisms/index.d.ts.map +1 -0
  299. package/lib/typescript/theme/UIProvider.d.ts +47 -0
  300. package/lib/typescript/theme/UIProvider.d.ts.map +1 -0
  301. package/lib/typescript/theme/createStyles.d.ts +21 -0
  302. package/lib/typescript/theme/createStyles.d.ts.map +1 -0
  303. package/lib/typescript/theme/createTheme.d.ts +9 -0
  304. package/lib/typescript/theme/createTheme.d.ts.map +1 -0
  305. package/lib/typescript/theme/index.d.ts +14 -0
  306. package/lib/typescript/theme/index.d.ts.map +1 -0
  307. package/lib/typescript/theme/insets.d.ts +9 -0
  308. package/lib/typescript/theme/insets.d.ts.map +1 -0
  309. package/lib/typescript/theme/labels.d.ts +13 -0
  310. package/lib/typescript/theme/labels.d.ts.map +1 -0
  311. package/lib/typescript/theme/palettes.d.ts +24 -0
  312. package/lib/typescript/theme/palettes.d.ts.map +1 -0
  313. package/lib/typescript/theme/tokens.d.ts +41 -0
  314. package/lib/typescript/theme/tokens.d.ts.map +1 -0
  315. package/lib/typescript/theme/types.d.ts +24 -0
  316. package/lib/typescript/theme/types.d.ts.map +1 -0
  317. package/package.json +311 -0
  318. package/registry/alert-provider.json +21 -0
  319. package/registry/alert.json +19 -0
  320. package/registry/avatar-header.json +18 -0
  321. package/registry/avatar.json +18 -0
  322. package/registry/badge.json +18 -0
  323. package/registry/bottom-sheet.json +18 -0
  324. package/registry/button.json +18 -0
  325. package/registry/card.json +18 -0
  326. package/registry/checkbox.json +18 -0
  327. package/registry/core.json +64 -0
  328. package/registry/date-field.json +24 -0
  329. package/registry/date-picker.json +23 -0
  330. package/registry/divider.json +18 -0
  331. package/registry/empty-state.json +18 -0
  332. package/registry/fab.json +19 -0
  333. package/registry/form-actions.json +19 -0
  334. package/registry/icon-button.json +19 -0
  335. package/registry/image.json +20 -0
  336. package/registry/index.json +465 -0
  337. package/registry/info-row.json +19 -0
  338. package/registry/input.json +18 -0
  339. package/registry/key-value-row.json +18 -0
  340. package/registry/list.json +20 -0
  341. package/registry/manifest.json +263 -0
  342. package/registry/modal.json +21 -0
  343. package/registry/option-card.json +18 -0
  344. package/registry/pressable-scale.json +18 -0
  345. package/registry/screen.json +18 -0
  346. package/registry/search-list.json +23 -0
  347. package/registry/select.json +18 -0
  348. package/registry/spinner.json +18 -0
  349. package/registry/stepper.json +18 -0
  350. package/registry/steps.json +18 -0
  351. package/registry/theme-toggle.json +18 -0
  352. package/registry/title.json +21 -0
  353. package/registry/toast-provider.json +20 -0
  354. package/registry/toast.json +18 -0
  355. package/registry/wizard.json +20 -0
  356. package/src/atoms/Avatar.tsx +40 -0
  357. package/src/atoms/Badge.tsx +55 -0
  358. package/src/atoms/Button.tsx +186 -0
  359. package/src/atoms/Card.tsx +27 -0
  360. package/src/atoms/Checkbox.tsx +75 -0
  361. package/src/atoms/Divider.tsx +14 -0
  362. package/src/atoms/EmptyState.tsx +60 -0
  363. package/src/atoms/Fab.tsx +63 -0
  364. package/src/atoms/IconButton.tsx +53 -0
  365. package/src/atoms/Image.tsx +137 -0
  366. package/src/atoms/Input.tsx +166 -0
  367. package/src/atoms/KeyValueRow.tsx +48 -0
  368. package/src/atoms/PressableScale.tsx +52 -0
  369. package/src/atoms/Spinner.tsx +127 -0
  370. package/src/atoms/Text.tsx +58 -0
  371. package/src/atoms/index.ts +30 -0
  372. package/src/icons/Icon.tsx +27 -0
  373. package/src/icons/defaultIcons.tsx +292 -0
  374. package/src/icons/index.ts +4 -0
  375. package/src/icons/types.ts +27 -0
  376. package/src/index.ts +5 -0
  377. package/src/molecules/AvatarHeader.tsx +69 -0
  378. package/src/molecules/FormActions.tsx +52 -0
  379. package/src/molecules/InfoRow.tsx +52 -0
  380. package/src/molecules/OptionCard.tsx +82 -0
  381. package/src/molecules/Stepper.tsx +76 -0
  382. package/src/molecules/Steps.tsx +198 -0
  383. package/src/molecules/ThemeToggle.tsx +67 -0
  384. package/src/molecules/index.ts +14 -0
  385. package/src/organisms/Alert.tsx +162 -0
  386. package/src/organisms/AlertProvider.tsx +125 -0
  387. package/src/organisms/BottomSheet.tsx +95 -0
  388. package/src/organisms/DateField.tsx +134 -0
  389. package/src/organisms/DatePicker.tsx +255 -0
  390. package/src/organisms/List.tsx +154 -0
  391. package/src/organisms/Modal.tsx +251 -0
  392. package/src/organisms/Screen.tsx +84 -0
  393. package/src/organisms/SearchList.tsx +186 -0
  394. package/src/organisms/Select.tsx +156 -0
  395. package/src/organisms/Toast.tsx +135 -0
  396. package/src/organisms/ToastProvider.tsx +166 -0
  397. package/src/organisms/Wizard.tsx +137 -0
  398. package/src/organisms/index.ts +26 -0
  399. package/src/theme/UIProvider.tsx +129 -0
  400. package/src/theme/createStyles.ts +40 -0
  401. package/src/theme/createTheme.ts +49 -0
  402. package/src/theme/index.ts +22 -0
  403. package/src/theme/insets.ts +9 -0
  404. package/src/theme/labels.ts +23 -0
  405. package/src/theme/palettes.ts +53 -0
  406. package/src/theme/tokens.ts +113 -0
  407. package/src/theme/types.ts +27 -0
@@ -0,0 +1,251 @@
1
+ import React, { memo, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Dimensions,
5
+ KeyboardAvoidingView,
6
+ Modal as RNModal,
7
+ Platform,
8
+ Pressable,
9
+ ScrollView,
10
+ View,
11
+ type ModalProps as RNModalProps,
12
+ type ViewStyle,
13
+ } from 'react-native';
14
+ import { createStyles } from '../theme/createStyles';
15
+ import { useColors, useInsets } from '../theme/UIProvider';
16
+ import { Title } from '../atoms/Text';
17
+ import { IconButton } from '../atoms/IconButton';
18
+
19
+ const { height: SCREEN_HEIGHT } = Dimensions.get('window');
20
+
21
+ export type ModalVariant = 'full' | 'overlay' | 'fullScreen' | 'bottomSheet';
22
+
23
+ export interface ModalProps
24
+ extends Pick<RNModalProps, 'statusBarTranslucent' | 'onRequestClose' | 'hardwareAccelerated'> {
25
+ visible: boolean;
26
+ onClose?: () => void;
27
+ title?: string;
28
+ closeAccessibilityLabel?: string;
29
+ children: React.ReactNode;
30
+ footer?: React.ReactNode;
31
+ containerStyle?: ViewStyle;
32
+ contentStyle?: ViewStyle;
33
+ variant?: ModalVariant;
34
+ scrollable?: boolean;
35
+ }
36
+
37
+ const useStyles = createStyles((theme) => ({
38
+ flex1: { flex: 1 },
39
+ fullWrapper: { flex: 1, backgroundColor: theme.colors.background },
40
+ overlayContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: theme.tokens.spacing.xxl - 4 },
41
+ bottomSheetContainer: { justifyContent: 'flex-end', padding: 0 },
42
+ backdrop: { backgroundColor: theme.colors.overlay },
43
+ overlayCard: {
44
+ width: '100%',
45
+ maxHeight: '85%',
46
+ backgroundColor: theme.colors.surface,
47
+ borderRadius: theme.tokens.radius.xxl,
48
+ overflow: 'hidden',
49
+ ...theme.tokens.shadow.lg,
50
+ },
51
+ bottomSheetCard: {
52
+ width: '100%',
53
+ maxHeight: '90%',
54
+ backgroundColor: theme.colors.surface,
55
+ borderTopLeftRadius: theme.tokens.radius.xxl,
56
+ borderTopRightRadius: theme.tokens.radius.xxl,
57
+ overflow: 'hidden',
58
+ ...theme.tokens.shadow.lg,
59
+ },
60
+ header: {
61
+ flexDirection: 'row',
62
+ justifyContent: 'space-between',
63
+ alignItems: 'center',
64
+ padding: theme.tokens.spacing.xl,
65
+ borderBottomWidth: 1,
66
+ borderBottomColor: theme.colors.border,
67
+ backgroundColor: theme.colors.surface,
68
+ },
69
+ title: { fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold, flex: 1 },
70
+ closeButton: { marginLeft: theme.tokens.spacing.md },
71
+ contentWrapper: { flexShrink: 1, backgroundColor: theme.colors.surface },
72
+ scrollWrapper: { flexShrink: 1 },
73
+ scrollContent: { padding: theme.tokens.spacing.xl, flexGrow: 1 },
74
+ scrollContentFull: { padding: 0, flexGrow: 1 },
75
+ footer: {
76
+ padding: theme.tokens.spacing.xl,
77
+ borderTopWidth: 1,
78
+ borderTopColor: theme.colors.border,
79
+ backgroundColor: theme.colors.surface,
80
+ },
81
+ }));
82
+
83
+ /**
84
+ * Modal
85
+ * Un solo componente para todos los modales de la app: maneja scroll,
86
+ * teclado, insets y layout según `variant`. overlay/bottomSheet/full animan
87
+ * su entrada/salida con Animated (solo transform, nunca opacity sobre la
88
+ * tarjeta: animar opacity ahí dispara un bug de Android donde los hijos con
89
+ * elevation parpadean/redibujan). El fondo semitransparente sí anima opacity.
90
+ */
91
+ export const Modal = memo(
92
+ ({
93
+ visible,
94
+ onClose,
95
+ title,
96
+ closeAccessibilityLabel = 'Close',
97
+ children,
98
+ footer,
99
+ containerStyle,
100
+ contentStyle,
101
+ variant = 'full',
102
+ scrollable = true,
103
+ statusBarTranslucent = true,
104
+ onRequestClose,
105
+ hardwareAccelerated,
106
+ }: ModalProps) => {
107
+ const isFull = variant === 'full';
108
+ const isOverlay = variant === 'overlay';
109
+ const isFullScreen = variant === 'fullScreen';
110
+ const isBottomSheet = variant === 'bottomSheet';
111
+ const isAnimatedVariant = isOverlay || isBottomSheet || isFull;
112
+ const insets = useInsets();
113
+ const colors = useColors();
114
+ const styles = useStyles();
115
+
116
+ const [mounted, setMounted] = useState(!isAnimatedVariant && visible);
117
+ const backdropOpacity = useRef(new Animated.Value(0)).current;
118
+ const cardProgress = useRef(new Animated.Value(0)).current;
119
+
120
+ useEffect(() => {
121
+ if (!isAnimatedVariant) {
122
+ setMounted(visible);
123
+ return;
124
+ }
125
+ if (visible) {
126
+ setMounted(true);
127
+ Animated.timing(backdropOpacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
128
+ Animated.timing(cardProgress, { toValue: 1, duration: 250, useNativeDriver: true }).start();
129
+ } else {
130
+ Animated.timing(backdropOpacity, { toValue: 0, duration: 180, useNativeDriver: true }).start();
131
+ Animated.timing(cardProgress, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
132
+ if (finished) setMounted(false);
133
+ });
134
+ }
135
+ // eslint-disable-next-line react-hooks/exhaustive-deps
136
+ }, [visible, isAnimatedVariant]);
137
+
138
+ const cardAnimatedStyle =
139
+ isBottomSheet || isFull
140
+ ? { transform: [{ translateY: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [SCREEN_HEIGHT, 0] }) }] }
141
+ : {
142
+ transform: [
143
+ { translateY: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [-40, 0] }) },
144
+ { scale: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [0.9, 1] }) },
145
+ ],
146
+ };
147
+
148
+ const behavior = Platform.OS === 'ios' ? (isFull ? undefined : 'padding') : 'height';
149
+ const keyboardOffset = Platform.OS === 'ios' ? 0 : 20;
150
+
151
+ const renderHeader = () => {
152
+ if (!title && !onClose) return null;
153
+ return (
154
+ <View style={styles.header}>
155
+ {!!title && (
156
+ <Title style={styles.title} numberOfLines={1}>
157
+ {title}
158
+ </Title>
159
+ )}
160
+ {!title && <View style={styles.flex1} />}
161
+ {!!onClose && (
162
+ <IconButton iconName="close" size={24} color={colors.text} onPress={onClose} accessibilityLabel={closeAccessibilityLabel} style={styles.closeButton} />
163
+ )}
164
+ </View>
165
+ );
166
+ };
167
+
168
+ const renderMainContent = () => (
169
+ <View style={[styles.contentWrapper, isFull && styles.flex1]}>
170
+ {scrollable ? (
171
+ <ScrollView
172
+ style={[styles.scrollWrapper, isFull && styles.flex1]}
173
+ contentContainerStyle={[!isFull ? styles.scrollContent : styles.scrollContentFull, contentStyle]}
174
+ keyboardShouldPersistTaps="handled"
175
+ showsVerticalScrollIndicator={false}
176
+ bounces={Platform.OS === 'ios'}
177
+ automaticallyAdjustKeyboardInsets
178
+ >
179
+ {children}
180
+ </ScrollView>
181
+ ) : (
182
+ <View style={[!isFull ? styles.scrollContent : styles.scrollContentFull, isFull && styles.flex1, contentStyle]}>
183
+ {children}
184
+ </View>
185
+ )}
186
+ </View>
187
+ );
188
+
189
+ const renderFooter = () => (footer ? <View style={styles.footer}>{footer}</View> : null);
190
+
191
+ if (!mounted) return null;
192
+
193
+ return (
194
+ <RNModal
195
+ visible
196
+ onRequestClose={onRequestClose ?? onClose}
197
+ animationType={isAnimatedVariant ? 'none' : 'slide'}
198
+ // `full` anima su propia entrada (translateY) con animationType="none"
199
+ // y necesita `transparent` para que no se vea un fondo negro detrás
200
+ // mientras desliza. RN sólo admite `transparent` junto a
201
+ // 'overFullScreen' — con 'pageSheet' avisa que la combinación no está
202
+ // soportada. Por eso `full` usa overFullScreen: la presentación nativa
203
+ // de pageSheet sería redundante con nuestra animación de todos modos.
204
+ presentationStyle={
205
+ Platform.OS === 'ios' ? (isFullScreen ? 'fullScreen' : 'overFullScreen') : undefined
206
+ }
207
+ transparent={isOverlay || isBottomSheet || isFull}
208
+ statusBarTranslucent={statusBarTranslucent}
209
+ hardwareAccelerated={hardwareAccelerated}
210
+ >
211
+ <KeyboardAvoidingView behavior={behavior} keyboardVerticalOffset={keyboardOffset} style={styles.flex1}>
212
+ {isOverlay || isBottomSheet ? (
213
+ <View style={[styles.overlayContainer, isBottomSheet && styles.bottomSheetContainer]}>
214
+ {/* Sin accessibilityLabel propio: el header ya expone un botón "Close"
215
+ accesible; duplicar el label aquí generaría dos elementos con el
216
+ mismo nombre para lectores de pantalla. Tocar afuera es un atajo
217
+ puramente táctil. */}
218
+ <Animated.View style={[{ ...StyleSheetAbsoluteFill }, styles.backdrop, { opacity: backdropOpacity }]}>
219
+ <Pressable style={StyleSheetAbsoluteFill} onPress={onClose} testID="modal-backdrop" />
220
+ </Animated.View>
221
+
222
+ <Animated.View
223
+ style={[
224
+ isOverlay ? styles.overlayCard : styles.bottomSheetCard,
225
+ isBottomSheet && { paddingBottom: Math.max(insets.bottom, Platform.OS === 'ios' ? 20 : 0) },
226
+ containerStyle,
227
+ cardAnimatedStyle,
228
+ ]}
229
+ >
230
+ {renderHeader()}
231
+ {renderMainContent()}
232
+ {renderFooter()}
233
+ </Animated.View>
234
+ </View>
235
+ ) : (
236
+ <Animated.View style={[styles.fullWrapper, containerStyle, isFull && cardAnimatedStyle]}>
237
+ <View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
238
+ {renderHeader()}
239
+ {renderMainContent()}
240
+ {renderFooter()}
241
+ </View>
242
+ </Animated.View>
243
+ )}
244
+ </KeyboardAvoidingView>
245
+ </RNModal>
246
+ );
247
+ }
248
+ );
249
+ Modal.displayName = 'Modal';
250
+
251
+ const StyleSheetAbsoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
@@ -0,0 +1,84 @@
1
+ import React, { memo } from 'react';
2
+ import { KeyboardAvoidingView, Platform, ScrollView, View, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+ import { useInsets } from '../theme/UIProvider';
5
+
6
+ export type ScreenEdge = 'top' | 'bottom';
7
+
8
+ export interface ScreenProps {
9
+ children: React.ReactNode;
10
+ style?: ViewStyle;
11
+ scrollable?: boolean;
12
+ contentContainerStyle?: ViewStyle;
13
+ /** Qué insets de safe area aplicar. @default ['top', 'bottom'] */
14
+ edges?: ScreenEdge[];
15
+ /**
16
+ * Compensa el teclado con un KeyboardAvoidingView. Ponelo en false cuando
17
+ * el contenido ya trae su propio scroller que maneja el teclado (Wizard,
18
+ * List, SearchList): las dos compensaciones se suman y el campo enfocado
19
+ * termina empujado fuera de la pantalla.
20
+ * @default true
21
+ */
22
+ keyboardAvoiding?: boolean;
23
+ }
24
+
25
+ const useStyles = createStyles((theme) => ({
26
+ wrapper: { flex: 1, backgroundColor: theme.colors.background },
27
+ container: { flex: 1 },
28
+ contentContainer: { flexGrow: 1 },
29
+ }));
30
+
31
+ /**
32
+ * Screen
33
+ * Contenedor raíz de pantalla: fondo del theme, safe area (via insets del
34
+ * UIProvider), teclado y scroll opcional resueltos de forma consistente.
35
+ */
36
+ export const Screen = memo(({
37
+ children,
38
+ style,
39
+ scrollable = true,
40
+ contentContainerStyle,
41
+ edges = ['top', 'bottom'],
42
+ keyboardAvoiding = true,
43
+ }: ScreenProps) => {
44
+ const insets = useInsets();
45
+ const styles = useStyles();
46
+ const applyTop = edges.includes('top');
47
+ const applyBottom = edges.includes('bottom');
48
+
49
+ const KeyboardWrapper: React.ComponentType<any> = keyboardAvoiding ? KeyboardAvoidingView : View;
50
+ const keyboardProps = keyboardAvoiding
51
+ ? {
52
+ behavior: Platform.OS === 'ios' ? ('padding' as const) : ('height' as const),
53
+ keyboardVerticalOffset: Platform.OS === 'ios' ? 0 : 20,
54
+ }
55
+ : {};
56
+
57
+ return (
58
+ <View style={[styles.wrapper, style]}>
59
+ <KeyboardWrapper style={styles.container} {...keyboardProps}>
60
+ {scrollable ? (
61
+ <ScrollView
62
+ style={styles.container}
63
+ contentInsetAdjustmentBehavior="automatic"
64
+ automaticallyAdjustKeyboardInsets
65
+ contentContainerStyle={[
66
+ styles.contentContainer,
67
+ { paddingTop: applyTop ? insets.top : 0 },
68
+ Platform.OS === 'android' && applyBottom && { paddingBottom: insets.bottom },
69
+ contentContainerStyle,
70
+ ]}
71
+ keyboardShouldPersistTaps="handled"
72
+ >
73
+ {children}
74
+ </ScrollView>
75
+ ) : (
76
+ <View style={[styles.container, { paddingTop: applyTop ? insets.top : 0, paddingBottom: applyBottom ? insets.bottom : 0 }]}>
77
+ {children}
78
+ </View>
79
+ )}
80
+ </KeyboardWrapper>
81
+ </View>
82
+ );
83
+ });
84
+ Screen.displayName = 'Screen';
@@ -0,0 +1,186 @@
1
+ import React, { memo } from 'react';
2
+ import { ActivityIndicator, FlatList, Text, View, type FlatListProps, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+ import { useColors, useLabels } from '../theme/UIProvider';
5
+ import { Input } from '../atoms/Input';
6
+ import { EmptyState } from '../atoms/EmptyState';
7
+ import { Spinner } from '../atoms/Spinner';
8
+ import { IconButton } from '../atoms/IconButton';
9
+ import type { IconName } from '../icons/types';
10
+
11
+ export interface SearchListProps<T> {
12
+ /** Chrome opcional arriba del buscador (ej. back button + título). */
13
+ header?: React.ReactNode;
14
+
15
+ searchValue: string;
16
+ onSearchChange: (text: string) => void;
17
+ searchPlaceholder?: string;
18
+ searchIsLoading?: boolean;
19
+ onScanPress?: () => void;
20
+ scanAccessibilityLabel?: string;
21
+ extraActions?: React.ReactNode;
22
+
23
+ data: T[];
24
+ keyExtractor: (item: T) => string;
25
+ renderItem: FlatListProps<T>['renderItem'];
26
+
27
+ isLoading?: boolean;
28
+ /** 'replace' cambia toda la lista por un spinner; 'overlay' lo muestra encima manteniendo el layout. */
29
+ loadingMode?: 'replace' | 'overlay';
30
+ loadingText?: string;
31
+
32
+ onLoadMore?: () => void;
33
+ isLoadingMore?: boolean;
34
+ loadingMoreText?: string;
35
+ hasMore?: boolean;
36
+ noMoreText?: string;
37
+
38
+ emptyTitle?: string;
39
+ emptyDescription?: string;
40
+ emptyIconName?: IconName;
41
+
42
+ contentContainerStyle?: StyleProp<ViewStyle>;
43
+ ListComponent?: React.ComponentType<any>;
44
+ listProps?: Record<string, unknown>;
45
+ }
46
+
47
+ const useStyles = createStyles((theme) => ({
48
+ container: { flex: 1 },
49
+ searchRow: { flexDirection: 'row', alignItems: 'center', marginBottom: theme.tokens.spacing.lg - 1, gap: theme.tokens.spacing.sm + 2 },
50
+ searchInput: { flex: 1, marginBottom: 0 },
51
+ scanButton: { backgroundColor: theme.colors.primary, width: 50, height: 50, borderRadius: theme.tokens.radius.lg, ...theme.tokens.shadow.sm },
52
+ listArea: { flex: 1 },
53
+ listContent: { paddingBottom: theme.tokens.spacing.xl },
54
+ loadingOverlay: {
55
+ position: 'absolute',
56
+ top: 0,
57
+ left: 0,
58
+ right: 0,
59
+ bottom: 0,
60
+ backgroundColor: theme.colors.surface + 'B3',
61
+ zIndex: 10,
62
+ justifyContent: 'center',
63
+ alignItems: 'center',
64
+ },
65
+ footer: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center', gap: theme.tokens.spacing.md },
66
+ footerText: { color: theme.colors.textLight, fontSize: theme.tokens.fontSize.md },
67
+ }));
68
+
69
+ /**
70
+ * SearchList
71
+ * Buscador + lista + estados de carga/paginación/vacío. El contenido de
72
+ * cada ítem (renderItem) queda a cargo de quien la usa, ya que difiere por
73
+ * completo entre casos de uso.
74
+ */
75
+ export function SearchList<T>({
76
+ header,
77
+ searchValue,
78
+ onSearchChange,
79
+ searchPlaceholder,
80
+ searchIsLoading = false,
81
+ onScanPress,
82
+ scanAccessibilityLabel = 'Scan',
83
+ extraActions,
84
+ data,
85
+ keyExtractor,
86
+ renderItem,
87
+ isLoading = false,
88
+ loadingMode = 'replace',
89
+ loadingText,
90
+ onLoadMore,
91
+ isLoadingMore = false,
92
+ loadingMoreText,
93
+ hasMore = false,
94
+ noMoreText,
95
+ emptyTitle,
96
+ emptyDescription,
97
+ emptyIconName,
98
+ contentContainerStyle,
99
+ ListComponent = FlatList,
100
+ listProps,
101
+ }: SearchListProps<T>) {
102
+ const colors = useColors();
103
+ const styles = useStyles();
104
+ const labels = useLabels();
105
+ const resolvedSearchPlaceholder = searchPlaceholder ?? labels.search;
106
+ const resolvedLoadingText = loadingText ?? labels.loading;
107
+ const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;
108
+
109
+ const renderFooter = () => {
110
+ if (isLoadingMore) {
111
+ return (
112
+ <View style={styles.footer}>
113
+ <Spinner text={resolvedLoadingMoreText} fullscreen={false} />
114
+ </View>
115
+ );
116
+ }
117
+ if (noMoreText && !hasMore && data.length > 0) {
118
+ return (
119
+ <View style={styles.footer}>
120
+ <Text style={styles.footerText}>{noMoreText}</Text>
121
+ </View>
122
+ );
123
+ }
124
+ return null;
125
+ };
126
+
127
+ const showReplaceLoading = isLoading && loadingMode === 'replace' && data.length === 0;
128
+ const showOverlayLoading = isLoading && loadingMode === 'overlay' && data.length === 0;
129
+ const showEmpty = !isLoading && data.length === 0;
130
+
131
+ return (
132
+ <View style={styles.container}>
133
+ {header}
134
+
135
+ <View style={styles.searchRow}>
136
+ <Input
137
+ placeholder={resolvedSearchPlaceholder}
138
+ leftIconName="search"
139
+ value={searchValue}
140
+ onChangeText={onSearchChange}
141
+ rightIconName={searchValue.length > 0 ? 'close' : undefined}
142
+ onRightIconPress={() => onSearchChange('')}
143
+ isLoading={searchIsLoading}
144
+ containerStyle={styles.searchInput}
145
+ />
146
+ {onScanPress && (
147
+ <IconButton
148
+ iconName="search"
149
+ onPress={onScanPress}
150
+ color={colors.white}
151
+ style={styles.scanButton}
152
+ accessibilityLabel={scanAccessibilityLabel}
153
+ />
154
+ )}
155
+ {extraActions}
156
+ </View>
157
+
158
+ <View style={styles.listArea}>
159
+ {showOverlayLoading && (
160
+ <View style={styles.loadingOverlay}>
161
+ <ActivityIndicator size="large" color={colors.primary} />
162
+ <Text style={styles.footerText}>{resolvedLoadingText}</Text>
163
+ </View>
164
+ )}
165
+
166
+ {showReplaceLoading ? (
167
+ <Spinner text={resolvedLoadingText} />
168
+ ) : showEmpty ? (
169
+ <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} />
170
+ ) : (
171
+ <ListComponent
172
+ data={data}
173
+ keyExtractor={keyExtractor}
174
+ keyboardDismissMode="on-drag"
175
+ contentContainerStyle={contentContainerStyle ?? styles.listContent}
176
+ onEndReached={onLoadMore}
177
+ onEndReachedThreshold={0.2}
178
+ ListFooterComponent={renderFooter}
179
+ renderItem={renderItem}
180
+ {...listProps}
181
+ />
182
+ )}
183
+ </View>
184
+ </View>
185
+ );
186
+ }
@@ -0,0 +1,156 @@
1
+ import React, { memo, useState } from 'react';
2
+ import { ActivityIndicator, Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+ import { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';
5
+ import { Icon } from '../icons/Icon';
6
+
7
+ export interface SelectOption<T> {
8
+ label: string;
9
+ value: T;
10
+ }
11
+
12
+ export interface SelectProps<T> {
13
+ label?: string;
14
+ required?: boolean;
15
+ options: SelectOption<T>[];
16
+ selectedValue: T | undefined;
17
+ onSelect: (value: T) => void;
18
+ placeholder?: string;
19
+ error?: string;
20
+ isLoading?: boolean;
21
+ containerStyle?: StyleProp<ViewStyle>;
22
+ }
23
+
24
+ const useStyles = createStyles((theme) => ({
25
+ container: { gap: theme.tokens.spacing.sm },
26
+ containerOpen: { zIndex: 20, elevation: 20 },
27
+ label: { fontWeight: theme.tokens.fontWeight.bold, fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
28
+ required: { color: theme.colors.error },
29
+ field: {
30
+ flexDirection: 'row',
31
+ alignItems: 'center',
32
+ justifyContent: 'space-between',
33
+ borderWidth: 2,
34
+ borderColor: theme.colors.border,
35
+ borderRadius: theme.tokens.radius.lg,
36
+ backgroundColor: theme.colors.inputBackground,
37
+ minHeight: 50,
38
+ paddingHorizontal: theme.tokens.spacing.lg,
39
+ },
40
+ fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text },
41
+ fieldPlaceholder: { color: theme.colors.textLight },
42
+ optionsList: {
43
+ position: 'absolute',
44
+ top: '100%',
45
+ left: 0,
46
+ right: 0,
47
+ marginTop: theme.tokens.spacing.sm,
48
+ borderWidth: 2,
49
+ borderColor: theme.colors.border,
50
+ borderRadius: theme.tokens.radius.lg,
51
+ backgroundColor: theme.colors.surface,
52
+ overflow: 'hidden',
53
+ zIndex: 20,
54
+ elevation: 20,
55
+ ...theme.tokens.shadow.md,
56
+ },
57
+ optionRow: {
58
+ flexDirection: 'row',
59
+ alignItems: 'center',
60
+ justifyContent: 'space-between',
61
+ paddingHorizontal: theme.tokens.spacing.lg,
62
+ paddingVertical: theme.tokens.spacing.md + 2,
63
+ borderBottomWidth: 1,
64
+ borderBottomColor: theme.colors.border,
65
+ },
66
+ optionRowLast: { borderBottomWidth: 0 },
67
+ optionRowSelected: { backgroundColor: theme.colors.primary + '15' },
68
+ optionText: { fontSize: theme.tokens.fontSize.lg - 1, color: theme.colors.text },
69
+ optionTextSelected: { fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.primary },
70
+ errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
71
+ }));
72
+
73
+ /**
74
+ * Select
75
+ * Dropdown que expande la lista de opciones flotando sobre el resto del
76
+ * contenido (position: absolute), en vez de empujarlo hacia abajo o abrir
77
+ * un picker nativo del sistema.
78
+ */
79
+ export function Select<T extends string | number>({
80
+ label,
81
+ required = false,
82
+ options,
83
+ selectedValue,
84
+ onSelect,
85
+ placeholder,
86
+ error,
87
+ isLoading = false,
88
+ containerStyle,
89
+ }: SelectProps<T>) {
90
+ const colors = useColors();
91
+ const styles = useStyles();
92
+ const labels = useLabels();
93
+ const allowFontScaling = useAllowFontScaling();
94
+ const [open, setOpen] = useState(false);
95
+ const selectedOption = options.find((o) => o.value === selectedValue);
96
+
97
+ return (
98
+ <View style={[styles.container, open && styles.containerOpen, containerStyle]}>
99
+ {label && (
100
+ <Text allowFontScaling={allowFontScaling} style={styles.label}>
101
+ {label}
102
+ {required && <Text style={styles.required}> *</Text>}
103
+ </Text>
104
+ )}
105
+
106
+ <Pressable
107
+ style={styles.field}
108
+ onPress={() => !isLoading && setOpen((o) => !o)}
109
+ disabled={isLoading}
110
+ accessibilityRole="button"
111
+ accessibilityLabel={label ?? placeholder ?? labels.selectPlaceholder}
112
+ accessibilityState={{ expanded: open, disabled: isLoading }}
113
+ >
114
+ <Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !selectedOption && styles.fieldPlaceholder]}>
115
+ {selectedOption?.label || placeholder || labels.selectPlaceholder}
116
+ </Text>
117
+ {isLoading ? (
118
+ <ActivityIndicator size="small" color={colors.primary} />
119
+ ) : (
120
+ <Icon name={open ? 'chevron-up' : 'chevron-down'} size={20} color={colors.textLight} />
121
+ )}
122
+ </Pressable>
123
+
124
+ {open && !isLoading && (
125
+ <View style={styles.optionsList}>
126
+ {options.map((option, index) => {
127
+ const isSelected = option.value === selectedValue;
128
+ return (
129
+ <Pressable
130
+ key={String(option.value)}
131
+ style={[styles.optionRow, index === options.length - 1 && styles.optionRowLast, isSelected && styles.optionRowSelected]}
132
+ onPress={() => {
133
+ onSelect(option.value);
134
+ setOpen(false);
135
+ }}
136
+ accessibilityRole="menuitem"
137
+ accessibilityState={{ selected: isSelected }}
138
+ >
139
+ <Text allowFontScaling={allowFontScaling} style={[styles.optionText, isSelected && styles.optionTextSelected]}>
140
+ {option.label}
141
+ </Text>
142
+ {isSelected && <Icon name="check" size={18} color={colors.primary} />}
143
+ </Pressable>
144
+ );
145
+ })}
146
+ </View>
147
+ )}
148
+
149
+ {!!error && (
150
+ <Text allowFontScaling={allowFontScaling} style={styles.errorText}>
151
+ {error}
152
+ </Text>
153
+ )}
154
+ </View>
155
+ );
156
+ }