orn-ui 0.1.0 → 0.2.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 (307) hide show
  1. package/README.md +9 -2
  2. package/lib/commonjs/atoms/Avatar.js +1 -1
  3. package/lib/commonjs/atoms/Avatar.js.map +1 -1
  4. package/lib/commonjs/atoms/Badge.js +10 -8
  5. package/lib/commonjs/atoms/Badge.js.map +1 -1
  6. package/lib/commonjs/atoms/Button.js +8 -6
  7. package/lib/commonjs/atoms/Button.js.map +1 -1
  8. package/lib/commonjs/atoms/Checkbox.js +4 -1
  9. package/lib/commonjs/atoms/Checkbox.js.map +1 -1
  10. package/lib/commonjs/atoms/Fab.js +1 -1
  11. package/lib/commonjs/atoms/Fab.js.map +1 -1
  12. package/lib/commonjs/atoms/Image.js +2 -2
  13. package/lib/commonjs/atoms/Image.js.map +1 -1
  14. package/lib/commonjs/atoms/Input.js +2 -2
  15. package/lib/commonjs/atoms/Input.js.map +1 -1
  16. package/lib/commonjs/atoms/PressableScale.js +9 -10
  17. package/lib/commonjs/atoms/PressableScale.js.map +1 -1
  18. package/lib/commonjs/atoms/Skeleton.js +96 -0
  19. package/lib/commonjs/atoms/Skeleton.js.map +1 -0
  20. package/lib/commonjs/atoms/Transition.js +171 -0
  21. package/lib/commonjs/atoms/Transition.js.map +1 -0
  22. package/lib/commonjs/atoms/index.js +20 -0
  23. package/lib/commonjs/atoms/index.js.map +1 -1
  24. package/lib/commonjs/molecules/AvatarHeader.js +2 -2
  25. package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
  26. package/lib/commonjs/molecules/OptionCard.js +57 -24
  27. package/lib/commonjs/molecules/OptionCard.js.map +1 -1
  28. package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
  29. package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
  30. package/lib/commonjs/molecules/Stepper.js +124 -47
  31. package/lib/commonjs/molecules/Stepper.js.map +1 -1
  32. package/lib/commonjs/molecules/Steps.js +2 -2
  33. package/lib/commonjs/molecules/Steps.js.map +1 -1
  34. package/lib/commonjs/molecules/index.js +9 -3
  35. package/lib/commonjs/molecules/index.js.map +1 -1
  36. package/lib/commonjs/organisms/Alert.js +6 -5
  37. package/lib/commonjs/organisms/Alert.js.map +1 -1
  38. package/lib/commonjs/organisms/AlertProvider.js +26 -0
  39. package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
  40. package/lib/commonjs/organisms/BottomSheet.js +118 -39
  41. package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
  42. package/lib/commonjs/organisms/DateField.js +2 -2
  43. package/lib/commonjs/organisms/DateField.js.map +1 -1
  44. package/lib/commonjs/organisms/DatePicker.js +3 -3
  45. package/lib/commonjs/organisms/DatePicker.js.map +1 -1
  46. package/lib/commonjs/organisms/List.js +66 -44
  47. package/lib/commonjs/organisms/List.js.map +1 -1
  48. package/lib/commonjs/organisms/Modal.js +42 -87
  49. package/lib/commonjs/organisms/Modal.js.map +1 -1
  50. package/lib/commonjs/organisms/NavigationBar.js +205 -0
  51. package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
  52. package/lib/commonjs/organisms/Screen.js +9 -2
  53. package/lib/commonjs/organisms/Screen.js.map +1 -1
  54. package/lib/commonjs/organisms/SearchList.js +99 -30
  55. package/lib/commonjs/organisms/SearchList.js.map +1 -1
  56. package/lib/commonjs/organisms/Select.js +226 -54
  57. package/lib/commonjs/organisms/Select.js.map +1 -1
  58. package/lib/commonjs/organisms/ThemeToggle.js +52 -0
  59. package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
  60. package/lib/commonjs/organisms/Toast.js +20 -25
  61. package/lib/commonjs/organisms/Toast.js.map +1 -1
  62. package/lib/commonjs/organisms/ToastProvider.js +24 -0
  63. package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
  64. package/lib/commonjs/organisms/Wizard.js +22 -2
  65. package/lib/commonjs/organisms/Wizard.js.map +1 -1
  66. package/lib/commonjs/organisms/imperativeBridge.js +28 -0
  67. package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
  68. package/lib/commonjs/organisms/index.js +50 -0
  69. package/lib/commonjs/organisms/index.js.map +1 -1
  70. package/lib/commonjs/safe-area/SafeAreaUIProvider.js +65 -0
  71. package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -0
  72. package/lib/commonjs/safe-area/index.js +13 -0
  73. package/lib/commonjs/safe-area/index.js.map +1 -0
  74. package/lib/commonjs/theme/UIProvider.js +11 -0
  75. package/lib/commonjs/theme/UIProvider.js.map +1 -1
  76. package/lib/commonjs/theme/colors.js +207 -0
  77. package/lib/commonjs/theme/colors.js.map +1 -0
  78. package/lib/commonjs/theme/createTheme.js +25 -2
  79. package/lib/commonjs/theme/createTheme.js.map +1 -1
  80. package/lib/commonjs/theme/index.js +25 -0
  81. package/lib/commonjs/theme/index.js.map +1 -1
  82. package/lib/commonjs/theme/palettes.js +73 -11
  83. package/lib/commonjs/theme/palettes.js.map +1 -1
  84. package/lib/module/atoms/Avatar.js +1 -1
  85. package/lib/module/atoms/Avatar.js.map +1 -1
  86. package/lib/module/atoms/Badge.js +10 -8
  87. package/lib/module/atoms/Badge.js.map +1 -1
  88. package/lib/module/atoms/Button.js +8 -6
  89. package/lib/module/atoms/Button.js.map +1 -1
  90. package/lib/module/atoms/Checkbox.js +4 -1
  91. package/lib/module/atoms/Checkbox.js.map +1 -1
  92. package/lib/module/atoms/Fab.js +1 -1
  93. package/lib/module/atoms/Fab.js.map +1 -1
  94. package/lib/module/atoms/Image.js +2 -2
  95. package/lib/module/atoms/Image.js.map +1 -1
  96. package/lib/module/atoms/Input.js +2 -2
  97. package/lib/module/atoms/Input.js.map +1 -1
  98. package/lib/module/atoms/PressableScale.js +9 -10
  99. package/lib/module/atoms/PressableScale.js.map +1 -1
  100. package/lib/module/atoms/Skeleton.js +91 -0
  101. package/lib/module/atoms/Skeleton.js.map +1 -0
  102. package/lib/module/atoms/Transition.js +165 -0
  103. package/lib/module/atoms/Transition.js.map +1 -0
  104. package/lib/module/atoms/index.js +2 -0
  105. package/lib/module/atoms/index.js.map +1 -1
  106. package/lib/module/molecules/AvatarHeader.js +2 -2
  107. package/lib/module/molecules/AvatarHeader.js.map +1 -1
  108. package/lib/module/molecules/OptionCard.js +58 -25
  109. package/lib/module/molecules/OptionCard.js.map +1 -1
  110. package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
  111. package/lib/module/molecules/SegmentedControl.js.map +1 -0
  112. package/lib/module/molecules/Stepper.js +125 -49
  113. package/lib/module/molecules/Stepper.js.map +1 -1
  114. package/lib/module/molecules/Steps.js +2 -2
  115. package/lib/module/molecules/Steps.js.map +1 -1
  116. package/lib/module/molecules/index.js +2 -2
  117. package/lib/module/molecules/index.js.map +1 -1
  118. package/lib/module/organisms/Alert.js +6 -5
  119. package/lib/module/organisms/Alert.js.map +1 -1
  120. package/lib/module/organisms/AlertProvider.js +24 -1
  121. package/lib/module/organisms/AlertProvider.js.map +1 -1
  122. package/lib/module/organisms/BottomSheet.js +120 -41
  123. package/lib/module/organisms/BottomSheet.js.map +1 -1
  124. package/lib/module/organisms/DateField.js +2 -2
  125. package/lib/module/organisms/DateField.js.map +1 -1
  126. package/lib/module/organisms/DatePicker.js +3 -3
  127. package/lib/module/organisms/DatePicker.js.map +1 -1
  128. package/lib/module/organisms/List.js +68 -46
  129. package/lib/module/organisms/List.js.map +1 -1
  130. package/lib/module/organisms/Modal.js +44 -89
  131. package/lib/module/organisms/Modal.js.map +1 -1
  132. package/lib/module/organisms/NavigationBar.js +200 -0
  133. package/lib/module/organisms/NavigationBar.js.map +1 -0
  134. package/lib/module/organisms/Screen.js +9 -2
  135. package/lib/module/organisms/Screen.js.map +1 -1
  136. package/lib/module/organisms/SearchList.js +100 -31
  137. package/lib/module/organisms/SearchList.js.map +1 -1
  138. package/lib/module/organisms/Select.js +228 -56
  139. package/lib/module/organisms/Select.js.map +1 -1
  140. package/lib/module/organisms/ThemeToggle.js +47 -0
  141. package/lib/module/organisms/ThemeToggle.js.map +1 -0
  142. package/lib/module/organisms/Toast.js +22 -27
  143. package/lib/module/organisms/Toast.js.map +1 -1
  144. package/lib/module/organisms/ToastProvider.js +21 -0
  145. package/lib/module/organisms/ToastProvider.js.map +1 -1
  146. package/lib/module/organisms/Wizard.js +23 -3
  147. package/lib/module/organisms/Wizard.js.map +1 -1
  148. package/lib/module/organisms/imperativeBridge.js +24 -0
  149. package/lib/module/organisms/imperativeBridge.js.map +1 -0
  150. package/lib/module/organisms/index.js +4 -2
  151. package/lib/module/organisms/index.js.map +1 -1
  152. package/lib/module/safe-area/SafeAreaUIProvider.js +60 -0
  153. package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -0
  154. package/lib/module/safe-area/index.js +4 -0
  155. package/lib/module/safe-area/index.js.map +1 -0
  156. package/lib/module/theme/UIProvider.js +11 -0
  157. package/lib/module/theme/UIProvider.js.map +1 -1
  158. package/lib/module/theme/colors.js +194 -0
  159. package/lib/module/theme/colors.js.map +1 -0
  160. package/lib/module/theme/createTheme.js +25 -2
  161. package/lib/module/theme/createTheme.js.map +1 -1
  162. package/lib/module/theme/index.js +1 -0
  163. package/lib/module/theme/index.js.map +1 -1
  164. package/lib/module/theme/palettes.js +73 -11
  165. package/lib/module/theme/palettes.js.map +1 -1
  166. package/lib/typescript/atoms/Badge.d.ts.map +1 -1
  167. package/lib/typescript/atoms/Button.d.ts.map +1 -1
  168. package/lib/typescript/atoms/Checkbox.d.ts.map +1 -1
  169. package/lib/typescript/atoms/PressableScale.d.ts +3 -2
  170. package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
  171. package/lib/typescript/atoms/Skeleton.d.ts +29 -0
  172. package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
  173. package/lib/typescript/atoms/Transition.d.ts +51 -0
  174. package/lib/typescript/atoms/Transition.d.ts.map +1 -0
  175. package/lib/typescript/atoms/index.d.ts +4 -0
  176. package/lib/typescript/atoms/index.d.ts.map +1 -1
  177. package/lib/typescript/molecules/OptionCard.d.ts +11 -6
  178. package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
  179. package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
  180. package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
  181. package/lib/typescript/molecules/Stepper.d.ts +35 -3
  182. package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
  183. package/lib/typescript/molecules/index.d.ts +4 -4
  184. package/lib/typescript/molecules/index.d.ts.map +1 -1
  185. package/lib/typescript/organisms/Alert.d.ts.map +1 -1
  186. package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
  187. package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
  188. package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
  189. package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
  190. package/lib/typescript/organisms/List.d.ts +9 -2
  191. package/lib/typescript/organisms/List.d.ts.map +1 -1
  192. package/lib/typescript/organisms/Modal.d.ts +3 -5
  193. package/lib/typescript/organisms/Modal.d.ts.map +1 -1
  194. package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
  195. package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
  196. package/lib/typescript/organisms/Screen.d.ts +2 -1
  197. package/lib/typescript/organisms/Screen.d.ts.map +1 -1
  198. package/lib/typescript/organisms/SearchList.d.ts +21 -5
  199. package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
  200. package/lib/typescript/organisms/Select.d.ts +19 -4
  201. package/lib/typescript/organisms/Select.d.ts.map +1 -1
  202. package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
  203. package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
  204. package/lib/typescript/organisms/Toast.d.ts.map +1 -1
  205. package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
  206. package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
  207. package/lib/typescript/organisms/Wizard.d.ts +7 -1
  208. package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
  209. package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
  210. package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
  211. package/lib/typescript/organisms/index.d.ts +7 -3
  212. package/lib/typescript/organisms/index.d.ts.map +1 -1
  213. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +46 -0
  214. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -0
  215. package/lib/typescript/safe-area/index.d.ts +3 -0
  216. package/lib/typescript/safe-area/index.d.ts.map +1 -0
  217. package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
  218. package/lib/typescript/theme/colors.d.ts +44 -0
  219. package/lib/typescript/theme/colors.d.ts.map +1 -0
  220. package/lib/typescript/theme/createTheme.d.ts +3 -0
  221. package/lib/typescript/theme/createTheme.d.ts.map +1 -1
  222. package/lib/typescript/theme/index.d.ts +3 -1
  223. package/lib/typescript/theme/index.d.ts.map +1 -1
  224. package/lib/typescript/theme/palettes.d.ts +41 -3
  225. package/lib/typescript/theme/palettes.d.ts.map +1 -1
  226. package/lib/typescript/theme/types.d.ts +9 -0
  227. package/lib/typescript/theme/types.d.ts.map +1 -1
  228. package/package.json +101 -18
  229. package/registry/alert-provider.json +9 -2
  230. package/registry/alert.json +1 -1
  231. package/registry/avatar-header.json +1 -1
  232. package/registry/avatar.json +1 -1
  233. package/registry/badge.json +1 -1
  234. package/registry/bottom-sheet.json +6 -2
  235. package/registry/button.json +1 -1
  236. package/registry/checkbox.json +1 -1
  237. package/registry/core.json +9 -5
  238. package/registry/date-field.json +2 -1
  239. package/registry/date-picker.json +1 -1
  240. package/registry/fab.json +1 -1
  241. package/registry/image.json +1 -1
  242. package/registry/index.json +108 -39
  243. package/registry/input.json +1 -1
  244. package/registry/list.json +2 -1
  245. package/registry/manifest.json +44 -6
  246. package/registry/modal.json +3 -2
  247. package/registry/navigation-bar.json +18 -0
  248. package/registry/option-card.json +3 -2
  249. package/registry/pressable-scale.json +1 -1
  250. package/registry/screen.json +1 -1
  251. package/registry/search-list.json +2 -1
  252. package/registry/segmented-control.json +18 -0
  253. package/registry/select.json +1 -1
  254. package/registry/skeleton.json +18 -0
  255. package/registry/stepper.json +3 -2
  256. package/registry/steps.json +1 -1
  257. package/registry/theme-toggle.json +5 -4
  258. package/registry/toast-provider.json +11 -3
  259. package/registry/toast.json +3 -2
  260. package/registry/transition.json +19 -0
  261. package/registry/wizard.json +3 -2
  262. package/src/atoms/Avatar.tsx +1 -1
  263. package/src/atoms/Badge.tsx +6 -4
  264. package/src/atoms/Button.tsx +8 -6
  265. package/src/atoms/Checkbox.tsx +4 -1
  266. package/src/atoms/Fab.tsx +1 -1
  267. package/src/atoms/Image.tsx +2 -2
  268. package/src/atoms/Input.tsx +2 -2
  269. package/src/atoms/PressableScale.tsx +16 -6
  270. package/src/atoms/Skeleton.tsx +114 -0
  271. package/src/atoms/Transition.tsx +189 -0
  272. package/src/atoms/index.ts +4 -0
  273. package/src/molecules/AvatarHeader.tsx +2 -2
  274. package/src/molecules/OptionCard.tsx +64 -27
  275. package/src/molecules/SegmentedControl.tsx +84 -0
  276. package/src/molecules/Stepper.tsx +147 -32
  277. package/src/molecules/Steps.tsx +2 -2
  278. package/src/molecules/index.ts +4 -4
  279. package/src/organisms/Alert.tsx +6 -5
  280. package/src/organisms/AlertProvider.tsx +26 -1
  281. package/src/organisms/BottomSheet.tsx +214 -62
  282. package/src/organisms/DateField.tsx +2 -2
  283. package/src/organisms/DatePicker.tsx +3 -3
  284. package/src/organisms/List.tsx +73 -41
  285. package/src/organisms/Modal.tsx +49 -61
  286. package/src/organisms/NavigationBar.tsx +223 -0
  287. package/src/organisms/Screen.tsx +11 -3
  288. package/src/organisms/SearchList.tsx +134 -38
  289. package/src/organisms/Select.tsx +227 -49
  290. package/src/organisms/ThemeToggle.tsx +30 -0
  291. package/src/organisms/Toast.tsx +19 -24
  292. package/src/organisms/ToastProvider.tsx +24 -0
  293. package/src/organisms/Wizard.tsx +36 -3
  294. package/src/organisms/imperativeBridge.ts +28 -0
  295. package/src/organisms/index.ts +7 -3
  296. package/src/safe-area/SafeAreaUIProvider.tsx +67 -0
  297. package/src/safe-area/index.ts +2 -0
  298. package/src/theme/UIProvider.tsx +15 -0
  299. package/src/theme/colors.ts +175 -0
  300. package/src/theme/createTheme.ts +38 -4
  301. package/src/theme/index.ts +3 -1
  302. package/src/theme/palettes.ts +108 -11
  303. package/src/theme/types.ts +10 -0
  304. package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
  305. package/lib/module/molecules/ThemeToggle.js.map +0 -1
  306. package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
  307. package/src/molecules/ThemeToggle.tsx +0 -67
@@ -12,7 +12,7 @@
12
12
  "files": [
13
13
  {
14
14
  "path": "organisms/DatePicker.tsx",
15
- "content": "import React, { memo, useMemo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { IconButton } from '../atoms/IconButton';\n\nexport const DEFAULT_MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Índice 0 = domingo, para alinear con Date.getDay(). */\nexport const DEFAULT_WEEKDAY_NAMES = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];\n\nexport interface DateRange {\n start: Date;\n /** Falta mientras el rango está a medio elegir (sólo se tocó el inicio). */\n end?: Date;\n}\n\nexport interface DatePickerProps {\n /**\n * 'single' elige una fecha; 'range' elige un intervalo con dos toques.\n * @default 'single'\n */\n mode?: 'single' | 'range';\n /** Fecha elegida en mode=\"single\". */\n value?: Date;\n /** Se llama en mode=\"single\". */\n onChange?: (date: Date) => void;\n /** Rango elegido en mode=\"range\". */\n range?: DateRange;\n /** Se llama en mode=\"range\", tanto al fijar el inicio como al cerrar el rango. */\n onRangeChange?: (range: DateRange) => void;\n minDate?: Date;\n maxDate?: Date;\n /** Mes mostrado al abrir si no hay `value`. @default hoy */\n defaultMonth?: Date;\n monthNames?: string[];\n weekdayNames?: string[];\n /** 0 = domingo, 1 = lunes. @default 0 */\n firstDayOfWeek?: 0 | 1;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\n/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son \"iguales\". */\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nfunction startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n}\n\n/** Estrictamente entre start y end, sin incluir los extremos. */\nfunction isBetween(day: Date, start: Date, end: Date): boolean {\n const t = startOfDay(day).getTime();\n return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();\n}\n\nfunction isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {\n const t = startOfDay(day).getTime();\n if (minDate && t < startOfDay(minDate).getTime()) return true;\n if (maxDate && t > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Devuelve la grilla del mes: 6 filas de 7 celdas. Las celdas fuera del mes\n * son null, así el layout no salta de alto entre meses (un mes puede\n * necesitar 4, 5 o 6 filas según en qué día caiga el 1).\n */\nexport function buildMonthGrid(year: number, month: number, firstDayOfWeek: 0 | 1): (Date | null)[][] {\n const first = new Date(year, month, 1);\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const leading = (first.getDay() - firstDayOfWeek + 7) % 7;\n\n const cells: (Date | null)[] = [];\n for (let i = 0; i < leading; i++) cells.push(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));\n while (cells.length < 42) cells.push(null);\n\n const rows: (Date | null)[][] = [];\n for (let r = 0; r < 6; r++) rows.push(cells.slice(r * 7, r * 7 + 7));\n return rows;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n padding: theme.tokens.spacing.md,\n },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.tokens.spacing.sm },\n monthLabel: { fontSize: theme.tokens.fontSize.lg, fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.text },\n weekRow: { flexDirection: 'row' },\n weekday: {\n flex: 1,\n textAlign: 'center',\n fontSize: theme.tokens.fontSize.xs,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n paddingVertical: theme.tokens.spacing.sm,\n },\n row: { flexDirection: 'row' },\n cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },\n dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },\n dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n daySelected: { backgroundColor: theme.colors.primary },\n dayTextSelected: { color: theme.colors.white, fontWeight: theme.tokens.fontWeight.bold },\n // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y\n // no en el botón redondo, así la barra del rango se ve continua.\n cellInRange: { backgroundColor: theme.colors.primary + '1f' },\n cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },\n cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },\n dayTextInRange: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },\n dayToday: { borderWidth: 1, borderColor: theme.colors.primary },\n dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },\n}));\n\n/**\n * DatePicker\n * Calendario mensual dibujado con View/Text puros — sin\n * @react-native-community/datetimepicker ni ningún picker nativo, para\n * mantener la librería en cero dependencias y verse igual en iOS y Android.\n */\nexport const DatePicker = memo(\n ({\n mode = 'single',\n value,\n onChange,\n range,\n onRangeChange,\n minDate,\n maxDate,\n defaultMonth,\n monthNames = DEFAULT_MONTH_NAMES,\n weekdayNames = DEFAULT_WEEKDAY_NAMES,\n firstDayOfWeek = 0,\n style,\n testID,\n }: DatePickerProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const isRange = mode === 'range';\n\n const handlePress = (day: Date) => {\n if (!isRange) {\n onChange?.(day);\n return;\n }\n // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca\n // un rango nuevo. Segundo toque en adelante: lo cierra.\n if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {\n onRangeChange?.({ start: day });\n } else {\n onRangeChange?.({ start: range.start, end: day });\n }\n };\n\n const initial = value ?? range?.start ?? defaultMonth ?? new Date();\n const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));\n\n const today = useMemo(() => new Date(), []);\n const grid = useMemo(\n () => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),\n [visibleMonth, firstDayOfWeek]\n );\n\n const orderedWeekdays = useMemo(\n () => (firstDayOfWeek === 0 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),\n [weekdayNames, firstDayOfWeek]\n );\n\n const shiftMonth = (delta: number) =>\n setVisibleMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));\n\n const monthLabel = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.header}>\n <IconButton iconName=\"chevron-left\" accessibilityLabel=\"Previous month\" onPress={() => shiftMonth(-1)} />\n <Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole=\"header\">\n {monthLabel}\n </Text>\n <IconButton iconName=\"chevron-right\" accessibilityLabel=\"Next month\" onPress={() => shiftMonth(1)} />\n </View>\n\n <View style={styles.weekRow}>\n {orderedWeekdays.map((w, i) => (\n <Text allowFontScaling={allowFontScaling} key={`${w}-${i}`} style={styles.weekday}>\n {w}\n </Text>\n ))}\n </View>\n\n {grid.map((row, rowIndex) => (\n <View style={styles.row} key={rowIndex}>\n {row.map((day, cellIndex) => {\n if (!day) return <View style={styles.cell} key={cellIndex} />;\n\n const isStart = isRange && !!range?.start && isSameDay(day, range.start);\n const isEnd = isRange && !!range?.end && isSameDay(day, range.end);\n const inRange =\n isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);\n const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);\n\n const disabled = isOutOfRange(day, minDate, maxDate);\n const isToday = isSameDay(day, today);\n return (\n <View\n style={[\n styles.cell,\n (inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,\n isStart && !!range?.end && styles.cellRangeStart,\n isEnd && styles.cellRangeEnd,\n ]}\n key={cellIndex}\n >\n <TouchableOpacity\n style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}\n onPress={() => handlePress(day)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}\n accessibilityState={{ selected, disabled }}\n >\n <Text\n allowFontScaling={allowFontScaling}\n style={[\n styles.dayText,\n inRange && styles.dayTextInRange,\n selected && styles.dayTextSelected,\n disabled && styles.dayTextDisabled,\n ]}\n >\n {day.getDate()}\n </Text>\n </TouchableOpacity>\n </View>\n );\n })}\n </View>\n ))}\n </View>\n );\n }\n);\nDatePicker.displayName = 'DatePicker';\n"
15
+ "content": "import React, { memo, useMemo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { IconButton } from '../atoms/IconButton';\n\nexport const DEFAULT_MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Índice 0 = domingo, para alinear con Date.getDay(). */\nexport const DEFAULT_WEEKDAY_NAMES = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];\n\nexport interface DateRange {\n start: Date;\n /** Falta mientras el rango está a medio elegir (sólo se tocó el inicio). */\n end?: Date;\n}\n\nexport interface DatePickerProps {\n /**\n * 'single' elige una fecha; 'range' elige un intervalo con dos toques.\n * @default 'single'\n */\n mode?: 'single' | 'range';\n /** Fecha elegida en mode=\"single\". */\n value?: Date;\n /** Se llama en mode=\"single\". */\n onChange?: (date: Date) => void;\n /** Rango elegido en mode=\"range\". */\n range?: DateRange;\n /** Se llama en mode=\"range\", tanto al fijar el inicio como al cerrar el rango. */\n onRangeChange?: (range: DateRange) => void;\n minDate?: Date;\n maxDate?: Date;\n /** Mes mostrado al abrir si no hay `value`. @default hoy */\n defaultMonth?: Date;\n monthNames?: string[];\n weekdayNames?: string[];\n /** 0 = domingo, 1 = lunes. @default 0 */\n firstDayOfWeek?: 0 | 1;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\n/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son \"iguales\". */\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nfunction startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n}\n\n/** Estrictamente entre start y end, sin incluir los extremos. */\nfunction isBetween(day: Date, start: Date, end: Date): boolean {\n const t = startOfDay(day).getTime();\n return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();\n}\n\nfunction isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {\n const t = startOfDay(day).getTime();\n if (minDate && t < startOfDay(minDate).getTime()) return true;\n if (maxDate && t > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Devuelve la grilla del mes: 6 filas de 7 celdas. Las celdas fuera del mes\n * son null, así el layout no salta de alto entre meses (un mes puede\n * necesitar 4, 5 o 6 filas según en qué día caiga el 1).\n */\nexport function buildMonthGrid(year: number, month: number, firstDayOfWeek: 0 | 1): (Date | null)[][] {\n const first = new Date(year, month, 1);\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const leading = (first.getDay() - firstDayOfWeek + 7) % 7;\n\n const cells: (Date | null)[] = [];\n for (let i = 0; i < leading; i++) cells.push(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));\n while (cells.length < 42) cells.push(null);\n\n const rows: (Date | null)[][] = [];\n for (let r = 0; r < 6; r++) rows.push(cells.slice(r * 7, r * 7 + 7));\n return rows;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n padding: theme.tokens.spacing.md,\n },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.tokens.spacing.sm },\n monthLabel: { fontSize: theme.tokens.fontSize.lg, fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.text },\n weekRow: { flexDirection: 'row' },\n weekday: {\n flex: 1,\n textAlign: 'center',\n fontSize: theme.tokens.fontSize.xs,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n paddingVertical: theme.tokens.spacing.sm,\n },\n row: { flexDirection: 'row' },\n cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },\n dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },\n dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n daySelected: { backgroundColor: theme.colors.primary },\n dayTextSelected: { color: theme.colors.onPrimary, fontWeight: theme.tokens.fontWeight.bold },\n // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y\n // no en el botón redondo, así la barra del rango se ve continua.\n cellInRange: { backgroundColor: theme.colors.primarySoft },\n cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },\n cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },\n dayTextInRange: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },\n dayToday: { borderWidth: 1, borderColor: theme.colors.primary },\n dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },\n}));\n\n/**\n * DatePicker\n * Calendario mensual dibujado con View/Text puros — sin\n * @react-native-community/datetimepicker ni ningún picker nativo, para\n * mantener la librería en cero dependencias y verse igual en iOS y Android.\n */\nexport const DatePicker = memo(\n ({\n mode = 'single',\n value,\n onChange,\n range,\n onRangeChange,\n minDate,\n maxDate,\n defaultMonth,\n monthNames = DEFAULT_MONTH_NAMES,\n weekdayNames = DEFAULT_WEEKDAY_NAMES,\n firstDayOfWeek = 0,\n style,\n testID,\n }: DatePickerProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const isRange = mode === 'range';\n\n const handlePress = (day: Date) => {\n if (!isRange) {\n onChange?.(day);\n return;\n }\n // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca\n // un rango nuevo. Segundo toque en adelante: lo cierra.\n if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {\n onRangeChange?.({ start: day });\n } else {\n onRangeChange?.({ start: range.start, end: day });\n }\n };\n\n const initial = value ?? range?.start ?? defaultMonth ?? new Date();\n const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));\n\n const today = useMemo(() => new Date(), []);\n const grid = useMemo(\n () => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),\n [visibleMonth, firstDayOfWeek]\n );\n\n const orderedWeekdays = useMemo(\n () => (firstDayOfWeek === 0 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),\n [weekdayNames, firstDayOfWeek]\n );\n\n const shiftMonth = (delta: number) =>\n setVisibleMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));\n\n const monthLabel = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.header}>\n <IconButton iconName=\"chevron-left\" accessibilityLabel=\"Previous month\" onPress={() => shiftMonth(-1)} />\n <Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole=\"header\">\n {monthLabel}\n </Text>\n <IconButton iconName=\"chevron-right\" accessibilityLabel=\"Next month\" onPress={() => shiftMonth(1)} />\n </View>\n\n <View style={styles.weekRow}>\n {orderedWeekdays.map((w, i) => (\n <Text allowFontScaling={allowFontScaling} key={`${w}-${i}`} style={styles.weekday}>\n {w}\n </Text>\n ))}\n </View>\n\n {grid.map((row, rowIndex) => (\n <View style={styles.row} key={rowIndex}>\n {row.map((day, cellIndex) => {\n if (!day) return <View style={styles.cell} key={cellIndex} />;\n\n const isStart = isRange && !!range?.start && isSameDay(day, range.start);\n const isEnd = isRange && !!range?.end && isSameDay(day, range.end);\n const inRange =\n isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);\n const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);\n\n const disabled = isOutOfRange(day, minDate, maxDate);\n const isToday = isSameDay(day, today);\n return (\n <View\n style={[\n styles.cell,\n (inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,\n isStart && !!range?.end && styles.cellRangeStart,\n isEnd && styles.cellRangeEnd,\n ]}\n key={cellIndex}\n >\n <TouchableOpacity\n style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}\n onPress={() => handlePress(day)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}\n accessibilityState={{ selected, disabled }}\n >\n <Text\n allowFontScaling={allowFontScaling}\n style={[\n styles.dayText,\n inRange && styles.dayTextInRange,\n selected && styles.dayTextSelected,\n disabled && styles.dayTextDisabled,\n ]}\n >\n {day.getDate()}\n </Text>\n </TouchableOpacity>\n </View>\n );\n })}\n </View>\n ))}\n </View>\n );\n }\n);\nDatePicker.displayName = 'DatePicker';\n"
16
16
  }
17
17
  ],
18
18
  "registryDependencies": [
package/registry/fab.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "atoms/Fab.tsx",
12
- "content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface FabProps {\n onPress: (e: GestureResponderEvent) => void;\n iconName?: IconName;\n icon?: React.ReactNode;\n size?: number;\n bottom?: number;\n right?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n fab: {\n position: 'absolute',\n justifyContent: 'center',\n alignItems: 'center',\n ...theme.tokens.shadow.lg,\n },\n}));\n\n/**\n * Fab\n * Botón flotante circular. `bottom`/`right` en vez del `tabBarHeight`\n * obligatorio de la versión original — el consumidor decide su propio\n * offset (con o sin tab bar).\n */\nexport const Fab = memo(\n ({ onPress, iconName = 'plus', icon, size = 60, bottom = 20, right = 20, color, style, accessibilityLabel }: FabProps) => {\n const styles = useStyles();\n const colors = useColors();\n return (\n <PressableScale\n onPress={onPress}\n style={[\n styles.fab,\n {\n width: size,\n height: size,\n borderRadius: size / 2,\n bottom,\n right,\n backgroundColor: color ?? colors.primary,\n },\n style,\n ]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n >\n {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.white} />}\n </PressableScale>\n );\n }\n);\nFab.displayName = 'Fab';\n"
12
+ "content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface FabProps {\n onPress: (e: GestureResponderEvent) => void;\n iconName?: IconName;\n icon?: React.ReactNode;\n size?: number;\n bottom?: number;\n right?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n fab: {\n position: 'absolute',\n justifyContent: 'center',\n alignItems: 'center',\n ...theme.tokens.shadow.lg,\n },\n}));\n\n/**\n * Fab\n * Botón flotante circular. `bottom`/`right` en vez del `tabBarHeight`\n * obligatorio de la versión original — el consumidor decide su propio\n * offset (con o sin tab bar).\n */\nexport const Fab = memo(\n ({ onPress, iconName = 'plus', icon, size = 60, bottom = 20, right = 20, color, style, accessibilityLabel }: FabProps) => {\n const styles = useStyles();\n const colors = useColors();\n return (\n <PressableScale\n onPress={onPress}\n style={[\n styles.fab,\n {\n width: size,\n height: size,\n borderRadius: size / 2,\n bottom,\n right,\n backgroundColor: color ?? colors.primary,\n },\n style,\n ]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n >\n {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.onPrimary} />}\n </PressableScale>\n );\n }\n);\nFab.displayName = 'Fab';\n"
13
13
  }
14
14
  ],
15
15
  "registryDependencies": [
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "atoms/Image.tsx",
13
- "content": "import React, { memo, useCallback, useEffect, useState } from 'react';\nimport {\n Image as RNImage,\n Platform,\n StyleSheet,\n View,\n type ImageErrorEventData,\n type ImageResizeMode,\n type ImageSourcePropType,\n type NativeSyntheticEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Spinner } from './Spinner';\n\nexport interface ImageProps {\n source: ImageSourcePropType;\n width?: number;\n height?: number;\n /** @default 'cover' */\n resizeMode?: ImageResizeMode;\n /** Radio de las esquinas, mismo patrón que Avatar/Card. @default 0 */\n radius?: number;\n /** Qué mostrar mientras carga. `false` para no mostrar nada. @default true (spinner chico) */\n loading?: React.ReactNode | boolean;\n /** Qué mostrar si la carga falla. @default un placeholder gris, sin contenido */\n fallback?: React.ReactNode;\n /**\n * 'high' dispara Image.prefetch() al montar, antes de que <Image> arranque\n * su propio ciclo de carga — pensado para la imagen above-the-fold de una\n * pantalla (la primera que el usuario ve). Sin costo si ya está en caché.\n * @default 'normal'\n */\n priority?: 'low' | 'normal' | 'high';\n style?: StyleProp<ViewStyle>;\n onLoadEnd?: () => void;\n onError?: (error: NativeSyntheticEvent<ImageErrorEventData>) => void;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { overflow: 'hidden', backgroundColor: theme.colors.border + '30' },\n overlay: { alignItems: 'center', justifyContent: 'center' },\n fallback: { flex: 1, backgroundColor: theme.colors.border + '30' },\n}));\n\nfunction sourceUri(source: ImageSourcePropType): string | undefined {\n return typeof source === 'object' && source !== null && 'uri' in source ? source.uri : undefined;\n}\n\n/**\n * Image\n * Wrapper cero-dependencias sobre <Image> de RN: agrega estado de carga y\n * error (con los mismos placeholders que el resto de la librería), y expone\n * `priority=\"high\"` para adelantar la descarga vía Image.prefetch(). El\n * cache real (disco/memoria) lo maneja el motor de imágenes nativo de RN —\n * esto no lo reimplementa, solo no lo deja apagado por accidente: en iOS\n * fuerza `cache=\"force-cache\"` (usa el disco aunque el header HTTP no lo\n * pida); en Android el cache de disco de la imagen nativa ya está siempre\n * activo, no hay knob equivalente que tocar.\n */\nexport const Image = memo(\n ({\n source,\n width,\n height,\n resizeMode = 'cover',\n radius = 0,\n loading = true,\n fallback,\n priority = 'normal',\n style,\n onLoadEnd,\n onError,\n testID,\n }: ImageProps) => {\n const styles = useStyles();\n const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');\n const uri = sourceUri(source);\n\n useEffect(() => {\n // Resetea el estado si cambia la imagen (misma instancia de Image,\n // source distinto: p.ej. avatar de un usuario a otro en la misma fila).\n setStatus('loading');\n }, [uri ?? source]);\n\n useEffect(() => {\n if (priority === 'high' && uri) {\n RNImage.prefetch(uri).catch(() => {});\n }\n }, [priority, uri]);\n\n const handleError = useCallback(\n (e: NativeSyntheticEvent<ImageErrorEventData>) => {\n setStatus('error');\n onError?.(e);\n },\n [onError]\n );\n\n const handleLoadEnd = useCallback(() => {\n setStatus('loaded');\n onLoadEnd?.();\n }, [onLoadEnd]);\n\n return (\n <View style={[styles.container, { width, height, borderRadius: radius }, style]} testID={testID}>\n {status !== 'error' && (\n <RNImage\n source={source}\n resizeMode={resizeMode}\n style={StyleSheet.absoluteFill}\n fadeDuration={Platform.OS === 'android' ? 150 : undefined}\n {...(Platform.OS === 'ios' ? { cache: 'force-cache' as const } : null)}\n onLoadEnd={handleLoadEnd}\n onError={handleError}\n />\n )}\n {status === 'loading' && loading !== false && (\n <View style={[StyleSheet.absoluteFill, styles.overlay]}>\n {loading === true ? <Spinner size=\"small\" fullscreen={false} /> : loading}\n </View>\n )}\n {status === 'error' && (fallback ?? <View style={styles.fallback} />)}\n </View>\n );\n }\n);\nImage.displayName = 'Image';\n\n/**\n * Precarga una URL antes de montar el componente (p.ej. al recibir la\n * respuesta de un listado, antes de navegar al detalle). Re-exporta\n * Image.prefetch de RN — no hay nada zero-dep que agregarle encima.\n */\nexport const prefetchImage = RNImage.prefetch;\n"
13
+ "content": "import React, { memo, useCallback, useEffect, useState } from 'react';\nimport {\n Image as RNImage,\n Platform,\n StyleSheet,\n View,\n type ImageErrorEventData,\n type ImageResizeMode,\n type ImageSourcePropType,\n type NativeSyntheticEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Spinner } from './Spinner';\n\nexport interface ImageProps {\n source: ImageSourcePropType;\n width?: number;\n height?: number;\n /** @default 'cover' */\n resizeMode?: ImageResizeMode;\n /** Radio de las esquinas, mismo patrón que Avatar/Card. @default 0 */\n radius?: number;\n /** Qué mostrar mientras carga. `false` para no mostrar nada. @default true (spinner chico) */\n loading?: React.ReactNode | boolean;\n /** Qué mostrar si la carga falla. @default un placeholder gris, sin contenido */\n fallback?: React.ReactNode;\n /**\n * 'high' dispara Image.prefetch() al montar, antes de que <Image> arranque\n * su propio ciclo de carga — pensado para la imagen above-the-fold de una\n * pantalla (la primera que el usuario ve). Sin costo si ya está en caché.\n * @default 'normal'\n */\n priority?: 'low' | 'normal' | 'high';\n style?: StyleProp<ViewStyle>;\n onLoadEnd?: () => void;\n onError?: (error: NativeSyntheticEvent<ImageErrorEventData>) => void;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { overflow: 'hidden', backgroundColor: theme.colors.skeleton },\n overlay: { alignItems: 'center', justifyContent: 'center' },\n fallback: { flex: 1, backgroundColor: theme.colors.skeleton },\n}));\n\nfunction sourceUri(source: ImageSourcePropType): string | undefined {\n return typeof source === 'object' && source !== null && 'uri' in source ? source.uri : undefined;\n}\n\n/**\n * Image\n * Wrapper cero-dependencias sobre <Image> de RN: agrega estado de carga y\n * error (con los mismos placeholders que el resto de la librería), y expone\n * `priority=\"high\"` para adelantar la descarga vía Image.prefetch(). El\n * cache real (disco/memoria) lo maneja el motor de imágenes nativo de RN —\n * esto no lo reimplementa, solo no lo deja apagado por accidente: en iOS\n * fuerza `cache=\"force-cache\"` (usa el disco aunque el header HTTP no lo\n * pida); en Android el cache de disco de la imagen nativa ya está siempre\n * activo, no hay knob equivalente que tocar.\n */\nexport const Image = memo(\n ({\n source,\n width,\n height,\n resizeMode = 'cover',\n radius = 0,\n loading = true,\n fallback,\n priority = 'normal',\n style,\n onLoadEnd,\n onError,\n testID,\n }: ImageProps) => {\n const styles = useStyles();\n const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');\n const uri = sourceUri(source);\n\n useEffect(() => {\n // Resetea el estado si cambia la imagen (misma instancia de Image,\n // source distinto: p.ej. avatar de un usuario a otro en la misma fila).\n setStatus('loading');\n }, [uri ?? source]);\n\n useEffect(() => {\n if (priority === 'high' && uri) {\n RNImage.prefetch(uri).catch(() => {});\n }\n }, [priority, uri]);\n\n const handleError = useCallback(\n (e: NativeSyntheticEvent<ImageErrorEventData>) => {\n setStatus('error');\n onError?.(e);\n },\n [onError]\n );\n\n const handleLoadEnd = useCallback(() => {\n setStatus('loaded');\n onLoadEnd?.();\n }, [onLoadEnd]);\n\n return (\n <View style={[styles.container, { width, height, borderRadius: radius }, style]} testID={testID}>\n {status !== 'error' && (\n <RNImage\n source={source}\n resizeMode={resizeMode}\n style={StyleSheet.absoluteFill}\n fadeDuration={Platform.OS === 'android' ? 150 : undefined}\n {...(Platform.OS === 'ios' ? { cache: 'force-cache' as const } : null)}\n onLoadEnd={handleLoadEnd}\n onError={handleError}\n />\n )}\n {status === 'loading' && loading !== false && (\n <View style={[StyleSheet.absoluteFill, styles.overlay]}>\n {loading === true ? <Spinner size=\"small\" fullscreen={false} /> : loading}\n </View>\n )}\n {status === 'error' && (fallback ?? <View style={styles.fallback} />)}\n </View>\n );\n }\n);\nImage.displayName = 'Image';\n\n/**\n * Precarga una URL antes de montar el componente (p.ej. al recibir la\n * respuesta de un listado, antes de navegar al detalle). Re-exporta\n * Image.prefetch de RN — no hay nada zero-dep que agregarle encima.\n */\nexport const prefetchImage = RNImage.prefetch;\n"
14
14
  }
15
15
  ],
16
16
  "registryDependencies": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "core": {
3
- "bytes": 24213
3
+ "bytes": 35122
4
4
  },
5
5
  "components": [
6
6
  {
@@ -28,7 +28,7 @@
28
28
  "registryDependencies": [
29
29
  "core"
30
30
  ],
31
- "bytes": 6614
31
+ "bytes": 6790
32
32
  },
33
33
  {
34
34
  "name": "IconButton",
@@ -53,7 +53,7 @@
53
53
  "registryDependencies": [
54
54
  "core"
55
55
  ],
56
- "bytes": 5439
56
+ "bytes": 5447
57
57
  },
58
58
  {
59
59
  "name": "Checkbox",
@@ -65,7 +65,7 @@
65
65
  "registryDependencies": [
66
66
  "core"
67
67
  ],
68
- "bytes": 2632
68
+ "bytes": 2847
69
69
  },
70
70
  {
71
71
  "name": "Badge",
@@ -77,7 +77,7 @@
77
77
  "registryDependencies": [
78
78
  "core"
79
79
  ],
80
- "bytes": 2030
80
+ "bytes": 2172
81
81
  },
82
82
  {
83
83
  "name": "Card",
@@ -113,7 +113,7 @@
113
113
  "registryDependencies": [
114
114
  "core"
115
115
  ],
116
- "bytes": 1015
116
+ "bytes": 1012
117
117
  },
118
118
  {
119
119
  "name": "Image",
@@ -127,7 +127,7 @@
127
127
  "core",
128
128
  "spinner"
129
129
  ],
130
- "bytes": 4620
130
+ "bytes": 4610
131
131
  },
132
132
  {
133
133
  "name": "Spinner",
@@ -141,6 +141,31 @@
141
141
  ],
142
142
  "bytes": 4626
143
143
  },
144
+ {
145
+ "name": "Skeleton",
146
+ "slug": "skeleton",
147
+ "category": "atoms",
148
+ "exportedNames": [
149
+ "Skeleton"
150
+ ],
151
+ "registryDependencies": [
152
+ "core"
153
+ ],
154
+ "bytes": 3554
155
+ },
156
+ {
157
+ "name": "Transition",
158
+ "slug": "transition",
159
+ "category": "atoms",
160
+ "exportedNames": [
161
+ "Transition",
162
+ "useReduceMotion"
163
+ ],
164
+ "registryDependencies": [
165
+ "core"
166
+ ],
167
+ "bytes": 6558
168
+ },
144
169
  {
145
170
  "name": "EmptyState",
146
171
  "slug": "empty-state",
@@ -176,7 +201,7 @@
176
201
  "core",
177
202
  "pressable-scale"
178
203
  ],
179
- "bytes": 1784
204
+ "bytes": 1788
180
205
  },
181
206
  {
182
207
  "name": "PressableScale",
@@ -188,19 +213,20 @@
188
213
  "registryDependencies": [
189
214
  "core"
190
215
  ],
191
- "bytes": 1565
216
+ "bytes": 1709
192
217
  },
193
218
  {
194
219
  "name": "Stepper",
195
220
  "slug": "stepper",
196
221
  "category": "molecules",
197
222
  "exportedNames": [
198
- "Stepper"
223
+ "Stepper",
224
+ "sanitizeNumeric"
199
225
  ],
200
226
  "registryDependencies": [
201
227
  "core"
202
228
  ],
203
- "bytes": 2842
229
+ "bytes": 6717
204
230
  },
205
231
  {
206
232
  "name": "OptionCard",
@@ -210,9 +236,10 @@
210
236
  "OptionCard"
211
237
  ],
212
238
  "registryDependencies": [
213
- "core"
239
+ "core",
240
+ "pressable-scale"
214
241
  ],
215
- "bytes": 3005
242
+ "bytes": 4156
216
243
  },
217
244
  {
218
245
  "name": "InfoRow",
@@ -250,19 +277,19 @@
250
277
  "registryDependencies": [
251
278
  "core"
252
279
  ],
253
- "bytes": 2486
280
+ "bytes": 2487
254
281
  },
255
282
  {
256
- "name": "ThemeToggle",
257
- "slug": "theme-toggle",
283
+ "name": "SegmentedControl",
284
+ "slug": "segmented-control",
258
285
  "category": "molecules",
259
286
  "exportedNames": [
260
- "ThemeToggle"
287
+ "SegmentedControl"
261
288
  ],
262
289
  "registryDependencies": [
263
290
  "core"
264
291
  ],
265
- "bytes": 2440
292
+ "bytes": 2884
266
293
  },
267
294
  {
268
295
  "name": "Steps",
@@ -274,7 +301,7 @@
274
301
  "registryDependencies": [
275
302
  "core"
276
303
  ],
277
- "bytes": 7306
304
+ "bytes": 7314
278
305
  },
279
306
  {
280
307
  "name": "Modal",
@@ -287,9 +314,10 @@
287
314
  "core",
288
315
  "title",
289
316
  "icon-button",
290
- "pressable-scale"
317
+ "pressable-scale",
318
+ "transition"
291
319
  ],
292
- "bytes": 9588
320
+ "bytes": 8647
293
321
  },
294
322
  {
295
323
  "name": "BottomSheet",
@@ -299,9 +327,13 @@
299
327
  "BottomSheet"
300
328
  ],
301
329
  "registryDependencies": [
302
- "core"
330
+ "core",
331
+ "title",
332
+ "icon-button",
333
+ "pressable-scale",
334
+ "transition"
303
335
  ],
304
- "bytes": 3765
336
+ "bytes": 8662
305
337
  },
306
338
  {
307
339
  "name": "Select",
@@ -313,7 +345,7 @@
313
345
  "registryDependencies": [
314
346
  "core"
315
347
  ],
316
- "bytes": 5348
348
+ "bytes": 12122
317
349
  },
318
350
  {
319
351
  "name": "Alert",
@@ -326,7 +358,7 @@
326
358
  "core",
327
359
  "button"
328
360
  ],
329
- "bytes": 6138
361
+ "bytes": 6240
330
362
  },
331
363
  {
332
364
  "name": "AlertProvider",
@@ -334,14 +366,17 @@
334
366
  "category": "organisms",
335
367
  "exportedNames": [
336
368
  "AlertProvider",
337
- "useAlert"
369
+ "useAlert",
370
+ "showAlert",
371
+ "showConfirm",
372
+ "hideAlert"
338
373
  ],
339
374
  "registryDependencies": [
340
375
  "core",
341
376
  "alert",
342
377
  "button"
343
378
  ],
344
- "bytes": 3819
379
+ "bytes": 5470
345
380
  },
346
381
  {
347
382
  "name": "Screen",
@@ -353,7 +388,7 @@
353
388
  "registryDependencies": [
354
389
  "core"
355
390
  ],
356
- "bytes": 2814
391
+ "bytes": 3321
357
392
  },
358
393
  {
359
394
  "name": "List",
@@ -365,9 +400,10 @@
365
400
  "registryDependencies": [
366
401
  "core",
367
402
  "empty-state",
403
+ "skeleton",
368
404
  "spinner"
369
405
  ],
370
- "bytes": 4779
406
+ "bytes": 6039
371
407
  },
372
408
  {
373
409
  "name": "SearchList",
@@ -380,11 +416,12 @@
380
416
  "core",
381
417
  "input",
382
418
  "empty-state",
419
+ "skeleton",
383
420
  "spinner",
384
421
  "icon-button",
385
422
  "pressable-scale"
386
423
  ],
387
- "bytes": 5861
424
+ "bytes": 9327
388
425
  },
389
426
  {
390
427
  "name": "Toast",
@@ -394,9 +431,10 @@
394
431
  "Toast"
395
432
  ],
396
433
  "registryDependencies": [
397
- "core"
434
+ "core",
435
+ "transition"
398
436
  ],
399
- "bytes": 4411
437
+ "bytes": 4402
400
438
  },
401
439
  {
402
440
  "name": "ToastProvider",
@@ -404,13 +442,17 @@
404
442
  "category": "organisms",
405
443
  "exportedNames": [
406
444
  "ToastProvider",
407
- "useToast"
445
+ "useToast",
446
+ "showToast",
447
+ "hideToast",
448
+ "hideAllToasts"
408
449
  ],
409
450
  "registryDependencies": [
410
451
  "core",
411
- "toast"
452
+ "toast",
453
+ "transition"
412
454
  ],
413
- "bytes": 4920
455
+ "bytes": 6368
414
456
  },
415
457
  {
416
458
  "name": "DatePicker",
@@ -427,7 +469,7 @@
427
469
  "icon-button",
428
470
  "pressable-scale"
429
471
  ],
430
- "bytes": 10093
472
+ "bytes": 10098
431
473
  },
432
474
  {
433
475
  "name": "DateField",
@@ -442,10 +484,11 @@
442
484
  "title",
443
485
  "icon-button",
444
486
  "pressable-scale",
487
+ "transition",
445
488
  "date-picker",
446
489
  "button"
447
490
  ],
448
- "bytes": 4545
491
+ "bytes": 4553
449
492
  },
450
493
  {
451
494
  "name": "Wizard",
@@ -457,9 +500,35 @@
457
500
  "registryDependencies": [
458
501
  "core",
459
502
  "steps",
460
- "button"
503
+ "button",
504
+ "transition"
505
+ ],
506
+ "bytes": 5591
507
+ },
508
+ {
509
+ "name": "ThemeToggle",
510
+ "slug": "theme-toggle",
511
+ "category": "organisms",
512
+ "exportedNames": [
513
+ "ThemeToggle"
514
+ ],
515
+ "registryDependencies": [
516
+ "core",
517
+ "segmented-control"
518
+ ],
519
+ "bytes": 1189
520
+ },
521
+ {
522
+ "name": "NavigationBar",
523
+ "slug": "navigation-bar",
524
+ "category": "organisms",
525
+ "exportedNames": [
526
+ "NavigationBar"
527
+ ],
528
+ "registryDependencies": [
529
+ "core"
461
530
  ],
462
- "bytes": 4569
531
+ "bytes": 7378
463
532
  }
464
533
  ]
465
534
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "atoms/Input.tsx",
12
- "content": "import React, { memo, useRef, useState } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n type StyleProp,\n type TextInputProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface InputProps extends Omit<TextInputProps, 'style'> {\n label?: string;\n /** Muestra un asterisco junto al label. Puramente visual, no valida nada. */\n required?: boolean;\n error?: string;\n containerStyle?: StyleProp<ViewStyle>;\n style?: TextInputProps['style'];\n leftIconName?: IconName;\n rightIconName?: IconName;\n onRightIconPress?: () => void;\n isPassword?: boolean;\n isLoading?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { marginBottom: theme.tokens.spacing.lg, width: '100%' },\n label: {\n marginBottom: theme.tokens.spacing.sm,\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.medium,\n color: theme.colors.text,\n },\n required: { color: theme.colors.error },\n inputWrapper: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: theme.colors.inputBackground,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 2,\n minHeight: 50,\n },\n input: {\n flex: 1,\n paddingVertical: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.lg,\n fontSize: theme.tokens.fontSize.lg,\n color: theme.colors.text,\n },\n leftIcon: { marginLeft: theme.tokens.spacing.lg, marginRight: -theme.tokens.spacing.xs },\n rightIcon: { padding: theme.tokens.spacing.sm + 2, marginRight: theme.tokens.spacing.xs },\n errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },\n}));\n\n/**\n * Input\n * El borde anima color entre border/primary/error con Animated (no\n * useNativeDriver: los navegadores de color en RN sin Fabric solo son\n * confiables en el hilo de JS; el costo es despreciable, se anima un borde).\n */\nexport const Input = memo(\n ({\n label,\n required = false,\n error,\n containerStyle,\n leftIconName,\n rightIconName,\n onRightIconPress,\n isPassword = false,\n isLoading = false,\n style,\n onFocus,\n onBlur,\n testID,\n ...props\n }: InputProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const [isFocused, setIsFocused] = useState(false);\n const [showPassword, setShowPassword] = useState(false);\n const focusProgress = useRef(new Animated.Value(0)).current;\n\n const handleFocus: TextInputProps['onFocus'] = (e) => {\n setIsFocused(true);\n Animated.timing(focusProgress, { toValue: 1, duration: 200, useNativeDriver: false }).start();\n onFocus?.(e);\n };\n\n const handleBlur: TextInputProps['onBlur'] = (e) => {\n setIsFocused(false);\n Animated.timing(focusProgress, { toValue: 0, duration: 200, useNativeDriver: false }).start();\n onBlur?.(e);\n };\n\n const borderColor = error\n ? colors.error\n : focusProgress.interpolate({ outputRange: [colors.border, colors.primary], inputRange: [0, 1] });\n\n return (\n <View style={[styles.container, containerStyle]} testID={testID}>\n {label && (\n <Text allowFontScaling={allowFontScaling} style={styles.label}>\n {label}\n {required && <Text style={styles.required}> *</Text>}\n </Text>\n )}\n\n <Animated.View style={[styles.inputWrapper, { borderColor }]}>\n {leftIconName && <Icon name={leftIconName} size={20} color={colors.textLight} style={styles.leftIcon} />}\n\n <TextInput\n style={[styles.input, style]}\n placeholderTextColor={colors.textLight}\n onFocus={handleFocus}\n onBlur={handleBlur}\n secureTextEntry={isPassword && !showPassword}\n allowFontScaling={allowFontScaling}\n textAlignVertical=\"center\"\n accessibilityLabel={label ?? props.placeholder}\n {...props}\n />\n\n {isLoading ? (\n <View style={styles.rightIcon}>\n <ActivityIndicator size=\"small\" color={colors.primary} />\n </View>\n ) : isPassword ? (\n <TouchableOpacity\n onPress={() => setShowPassword((v) => !v)}\n style={styles.rightIcon}\n accessibilityRole=\"button\"\n accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}\n >\n <Icon name={showPassword ? 'eye-off' : 'eye'} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : rightIconName ? (\n <TouchableOpacity\n onPress={onRightIconPress}\n style={styles.rightIcon}\n disabled={!onRightIconPress}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Clear\"\n >\n <Icon name={rightIconName} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : null}\n </Animated.View>\n\n {!!error && (\n <Text allowFontScaling={allowFontScaling} style={styles.errorText}>\n {error}\n </Text>\n )}\n </View>\n );\n }\n);\nInput.displayName = 'Input';\n"
12
+ "content": "import React, { memo, useRef, useState } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Text,\n TextInput,\n TouchableOpacity,\n View,\n type StyleProp,\n type TextInputProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface InputProps extends Omit<TextInputProps, 'style'> {\n label?: string;\n /** Muestra un asterisco junto al label. Puramente visual, no valida nada. */\n required?: boolean;\n error?: string;\n containerStyle?: StyleProp<ViewStyle>;\n style?: TextInputProps['style'];\n leftIconName?: IconName;\n rightIconName?: IconName;\n onRightIconPress?: () => void;\n isPassword?: boolean;\n isLoading?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { marginBottom: theme.tokens.spacing.lg, width: '100%' },\n label: {\n marginBottom: theme.tokens.spacing.sm,\n fontSize: theme.tokens.fontSize.md,\n fontWeight: theme.tokens.fontWeight.medium,\n color: theme.colors.text,\n },\n required: { color: theme.colors.errorText },\n inputWrapper: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: theme.colors.inputBackground,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 2,\n minHeight: 50,\n },\n input: {\n flex: 1,\n paddingVertical: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.lg,\n fontSize: theme.tokens.fontSize.lg,\n color: theme.colors.text,\n },\n leftIcon: { marginLeft: theme.tokens.spacing.lg, marginRight: -theme.tokens.spacing.xs },\n rightIcon: { padding: theme.tokens.spacing.sm + 2, marginRight: theme.tokens.spacing.xs },\n errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },\n}));\n\n/**\n * Input\n * El borde anima color entre border/primary/error con Animated (no\n * useNativeDriver: los navegadores de color en RN sin Fabric solo son\n * confiables en el hilo de JS; el costo es despreciable, se anima un borde).\n */\nexport const Input = memo(\n ({\n label,\n required = false,\n error,\n containerStyle,\n leftIconName,\n rightIconName,\n onRightIconPress,\n isPassword = false,\n isLoading = false,\n style,\n onFocus,\n onBlur,\n testID,\n ...props\n }: InputProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const [isFocused, setIsFocused] = useState(false);\n const [showPassword, setShowPassword] = useState(false);\n const focusProgress = useRef(new Animated.Value(0)).current;\n\n const handleFocus: TextInputProps['onFocus'] = (e) => {\n setIsFocused(true);\n Animated.timing(focusProgress, { toValue: 1, duration: 200, useNativeDriver: false }).start();\n onFocus?.(e);\n };\n\n const handleBlur: TextInputProps['onBlur'] = (e) => {\n setIsFocused(false);\n Animated.timing(focusProgress, { toValue: 0, duration: 200, useNativeDriver: false }).start();\n onBlur?.(e);\n };\n\n const borderColor = error\n ? colors.error\n : focusProgress.interpolate({ outputRange: [colors.border, colors.primary], inputRange: [0, 1] });\n\n return (\n <View style={[styles.container, containerStyle]} testID={testID}>\n {label && (\n <Text allowFontScaling={allowFontScaling} style={styles.label}>\n {label}\n {required && <Text style={styles.required}> *</Text>}\n </Text>\n )}\n\n <Animated.View style={[styles.inputWrapper, { borderColor }]}>\n {leftIconName && <Icon name={leftIconName} size={20} color={colors.textLight} style={styles.leftIcon} />}\n\n <TextInput\n style={[styles.input, style]}\n placeholderTextColor={colors.textLight}\n onFocus={handleFocus}\n onBlur={handleBlur}\n secureTextEntry={isPassword && !showPassword}\n allowFontScaling={allowFontScaling}\n textAlignVertical=\"center\"\n accessibilityLabel={label ?? props.placeholder}\n {...props}\n />\n\n {isLoading ? (\n <View style={styles.rightIcon}>\n <ActivityIndicator size=\"small\" color={colors.primary} />\n </View>\n ) : isPassword ? (\n <TouchableOpacity\n onPress={() => setShowPassword((v) => !v)}\n style={styles.rightIcon}\n accessibilityRole=\"button\"\n accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}\n >\n <Icon name={showPassword ? 'eye-off' : 'eye'} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : rightIconName ? (\n <TouchableOpacity\n onPress={onRightIconPress}\n style={styles.rightIcon}\n disabled={!onRightIconPress}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Clear\"\n >\n <Icon name={rightIconName} size={20} color={colors.textLight} />\n </TouchableOpacity>\n ) : null}\n </Animated.View>\n\n {!!error && (\n <Text allowFontScaling={allowFontScaling} style={styles.errorText}>\n {error}\n </Text>\n )}\n </View>\n );\n }\n);\nInput.displayName = 'Input';\n"
13
13
  }
14
14
  ],
15
15
  "registryDependencies": [
@@ -9,12 +9,13 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "organisms/List.tsx",
12
- "content": "import React, { memo, useCallback } from 'react';\nimport {\n ActivityIndicator,\n FlatList,\n Platform,\n RefreshControl,\n Text,\n View,\n type FlatListProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useLabels } from '../theme/UIProvider';\nimport { EmptyState } from '../atoms/EmptyState';\nimport { Spinner } from '../atoms/Spinner';\nimport type { IconName } from '../icons/types';\n\nexport interface ListProps<T> {\n data: T[];\n keyExtractor: (item: T) => string;\n renderItem: FlatListProps<T>['renderItem'];\n\n isLoading: boolean;\n isLoadingMore?: boolean;\n /** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */\n isReady?: boolean;\n loadingText?: string;\n loadingMoreText?: string;\n\n onEndReached?: () => void;\n onEndReachedThreshold?: number;\n onRefresh?: () => void;\n\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIconName?: IconName;\n\n ListHeaderComponent?: FlatListProps<T>['ListHeaderComponent'];\n ListFooterComponent?: FlatListProps<T>['ListFooterComponent'];\n\n contentContainerStyle?: ViewStyle;\n containerStyle?: ViewStyle;\n\n /** Componente de lista a usar (FlashList, etc). @default FlatList */\n ListComponent?: React.ComponentType<any>;\n listProps?: Record<string, unknown>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, backgroundColor: theme.colors.background },\n loadingOverlay: {\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: theme.colors.background + 'CC',\n zIndex: 10,\n justifyContent: 'center',\n alignItems: 'center',\n },\n loadingText: { marginTop: theme.tokens.spacing.md, color: theme.colors.textLight, fontWeight: theme.tokens.fontWeight.medium },\n listContent: { padding: theme.tokens.spacing.md, paddingBottom: Platform.OS === 'android' ? 100 : 80 },\n footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },\n}));\n\n/**\n * List\n * Lista con estados de carga/paginación/vacío resueltos, sin atarse a\n * expo-router ni a FlashList: `ListComponent` permite inyectar cualquiera\n * (por defecto FlatList del propio React Native).\n */\nexport function List<T>({\n data,\n keyExtractor,\n renderItem,\n isLoading,\n isLoadingMore = false,\n isReady = true,\n loadingText,\n loadingMoreText,\n onEndReached,\n onEndReachedThreshold = 0.3,\n onRefresh,\n emptyTitle,\n emptyDescription,\n emptyIconName,\n ListHeaderComponent,\n ListFooterComponent,\n contentContainerStyle,\n containerStyle,\n ListComponent = FlatList,\n listProps,\n}: ListProps<T>) {\n const colors = useColors();\n const styles = useStyles();\n const labels = useLabels();\n const resolvedLoadingText = loadingText ?? labels.loading;\n const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;\n\n const renderFooter = useCallback(() => {\n if (ListFooterComponent) {\n return typeof ListFooterComponent === 'function' ? <ListFooterComponent /> : (ListFooterComponent as React.ReactElement);\n }\n if (!isLoadingMore) return null;\n return (\n <View style={styles.footerLoader}>\n <Spinner text={resolvedLoadingMoreText} fullscreen={false} />\n </View>\n );\n }, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);\n\n const renderEmpty = useCallback(() => {\n if (isLoading) return null;\n return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;\n }, [isLoading, emptyTitle, emptyDescription, emptyIconName]);\n\n if (!isReady) {\n return (\n <View style={styles.container}>\n <Spinner text={resolvedLoadingText} />\n </View>\n );\n }\n\n return (\n <View style={[styles.container, containerStyle]}>\n {isLoading && data.length === 0 && (\n <View style={styles.loadingOverlay} pointerEvents=\"none\">\n <ActivityIndicator size=\"large\" color={colors.primary} />\n <Text style={styles.loadingText}>{resolvedLoadingText}</Text>\n </View>\n )}\n\n <ListComponent\n data={data}\n keyExtractor={keyExtractor}\n renderItem={renderItem}\n contentContainerStyle={[styles.listContent, contentContainerStyle]}\n onEndReached={onEndReached}\n onEndReachedThreshold={onEndReachedThreshold}\n ListHeaderComponent={ListHeaderComponent}\n ListFooterComponent={renderFooter}\n ListEmptyComponent={renderEmpty}\n refreshControl={\n onRefresh ? (\n <RefreshControl refreshing={isLoading} onRefresh={onRefresh} colors={[colors.primary]} tintColor={colors.primary} />\n ) : undefined\n }\n {...listProps}\n />\n </View>\n );\n}\n"
12
+ "content": "import React, { useCallback } from 'react';\nimport { FlatList, RefreshControl, View, type FlatListProps, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useLabels } from '../theme/UIProvider';\nimport { EmptyState } from '../atoms/EmptyState';\nimport { Skeleton } from '../atoms/Skeleton';\nimport { Spinner } from '../atoms/Spinner';\nimport type { IconName } from '../icons/types';\n\nexport interface ListProps<T> {\n data: T[];\n keyExtractor: (item: T, index: number) => string;\n renderItem: FlatListProps<T>['renderItem'];\n\n /** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */\n isLoading: boolean;\n /** Recarga con datos ya en pantalla (pull-to-refresh). @default false */\n isRefreshing?: boolean;\n isLoadingMore?: boolean;\n /** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */\n isReady?: boolean;\n loadingText?: string;\n loadingMoreText?: string;\n\n /** Filas fantasma de la carga inicial. @default 6 */\n skeletonCount?: number;\n /** Placeholder propio, para que calce con la forma real del ítem. */\n renderSkeletonItem?: () => React.ReactElement;\n\n onEndReached?: () => void;\n onEndReachedThreshold?: number;\n onRefresh?: () => void;\n\n emptyTitle?: string;\n emptyDescription?: string;\n emptyIconName?: IconName;\n\n ListHeaderComponent?: FlatListProps<T>['ListHeaderComponent'];\n ListFooterComponent?: FlatListProps<T>['ListFooterComponent'];\n\n contentContainerStyle?: ViewStyle;\n containerStyle?: ViewStyle;\n\n /** Componente de lista a usar (FlashList, etc). @default FlatList */\n ListComponent?: React.ComponentType<any>;\n listProps?: Record<string, unknown>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, backgroundColor: theme.colors.background },\n listContent: { padding: theme.tokens.spacing.md },\n footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },\n skeletonRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n padding: theme.tokens.spacing.md,\n },\n skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },\n}));\n\n/**\n * List\n * Lista con estados de carga/paginación/vacío resueltos, sin atarse a\n * expo-router ni a FlashList: `ListComponent` permite inyectar cualquiera\n * (por defecto FlatList del propio React Native).\n */\nexport function List<T>({\n data,\n keyExtractor,\n renderItem,\n isLoading,\n isRefreshing = false,\n isLoadingMore = false,\n isReady = true,\n loadingText,\n loadingMoreText,\n skeletonCount = 6,\n renderSkeletonItem,\n onEndReached,\n onEndReachedThreshold = 0.3,\n onRefresh,\n emptyTitle,\n emptyDescription,\n emptyIconName,\n ListHeaderComponent,\n ListFooterComponent,\n contentContainerStyle,\n containerStyle,\n ListComponent = FlatList,\n listProps,\n}: ListProps<T>) {\n const colors = useColors();\n const styles = useStyles();\n const labels = useLabels();\n const resolvedLoadingText = loadingText ?? labels.loading;\n const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;\n\n const isInitialLoading = isLoading && data.length === 0;\n\n // El footer propio y el loader de paginación conviven: antes el primero\n // tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.\n const renderFooter = useCallback(() => {\n const custom =\n typeof ListFooterComponent === 'function' ? (\n <ListFooterComponent />\n ) : (\n (ListFooterComponent as React.ReactElement | undefined) ?? null\n );\n\n return (\n <>\n {custom}\n {isLoadingMore && (\n <View style={styles.footerLoader}>\n <Spinner text={resolvedLoadingMoreText} fullscreen={false} />\n </View>\n )}\n </>\n );\n }, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);\n\n // Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se\n // muestran placeholders, no \"sin resultados\".\n const renderEmpty = useCallback(() => {\n if (isLoading || isRefreshing) return null;\n return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;\n }, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);\n\n if (!isReady) {\n return (\n <View style={styles.container}>\n <Spinner text={resolvedLoadingText} />\n </View>\n );\n }\n\n if (isInitialLoading) {\n return (\n <View style={[styles.container, containerStyle]} testID=\"list-skeleton\">\n <View style={[styles.listContent, contentContainerStyle]}>\n {Array.from({ length: skeletonCount }, (_, i) => (\n <React.Fragment key={i}>\n {renderSkeletonItem?.() ?? (\n <View style={styles.skeletonRow}>\n <Skeleton variant=\"circle\" width={40} />\n <View style={styles.skeletonText}>\n <Skeleton width=\"60%\" height={14} />\n <Skeleton width=\"35%\" height={12} />\n </View>\n </View>\n )}\n </React.Fragment>\n ))}\n </View>\n </View>\n );\n }\n\n return (\n <View style={[styles.container, containerStyle]}>\n <ListComponent\n data={data}\n keyExtractor={keyExtractor}\n renderItem={renderItem}\n contentContainerStyle={[styles.listContent, contentContainerStyle]}\n onEndReached={onEndReached}\n onEndReachedThreshold={onEndReachedThreshold}\n ListHeaderComponent={ListHeaderComponent}\n ListFooterComponent={renderFooter}\n ListEmptyComponent={renderEmpty}\n refreshControl={\n onRefresh ? (\n <RefreshControl\n refreshing={isRefreshing}\n onRefresh={onRefresh}\n colors={[colors.primary]}\n tintColor={colors.primary}\n />\n ) : undefined\n }\n {...listProps}\n />\n </View>\n );\n}\n"
13
13
  }
14
14
  ],
15
15
  "registryDependencies": [
16
16
  "core",
17
17
  "empty-state",
18
+ "skeleton",
18
19
  "spinner"
19
20
  ]
20
21
  }
@@ -83,6 +83,22 @@
83
83
  "file": "atoms/Spinner.tsx",
84
84
  "slug": "spinner"
85
85
  },
86
+ {
87
+ "name": "Skeleton",
88
+ "siblingNames": [],
89
+ "category": "atoms",
90
+ "file": "atoms/Skeleton.tsx",
91
+ "slug": "skeleton"
92
+ },
93
+ {
94
+ "name": "Transition",
95
+ "siblingNames": [
96
+ "useReduceMotion"
97
+ ],
98
+ "category": "atoms",
99
+ "file": "atoms/Transition.tsx",
100
+ "slug": "transition"
101
+ },
86
102
  {
87
103
  "name": "EmptyState",
88
104
  "siblingNames": [],
@@ -113,7 +129,9 @@
113
129
  },
114
130
  {
115
131
  "name": "Stepper",
116
- "siblingNames": [],
132
+ "siblingNames": [
133
+ "sanitizeNumeric"
134
+ ],
117
135
  "category": "molecules",
118
136
  "file": "molecules/Stepper.tsx",
119
137
  "slug": "stepper"
@@ -147,11 +165,11 @@
147
165
  "slug": "avatar-header"
148
166
  },
149
167
  {
150
- "name": "ThemeToggle",
168
+ "name": "SegmentedControl",
151
169
  "siblingNames": [],
152
170
  "category": "molecules",
153
- "file": "molecules/ThemeToggle.tsx",
154
- "slug": "theme-toggle"
171
+ "file": "molecules/SegmentedControl.tsx",
172
+ "slug": "segmented-control"
155
173
  },
156
174
  {
157
175
  "name": "Steps",
@@ -191,7 +209,10 @@
191
209
  {
192
210
  "name": "AlertProvider",
193
211
  "siblingNames": [
194
- "useAlert"
212
+ "useAlert",
213
+ "showAlert",
214
+ "showConfirm",
215
+ "hideAlert"
195
216
  ],
196
217
  "category": "organisms",
197
218
  "file": "organisms/AlertProvider.tsx",
@@ -228,7 +249,10 @@
228
249
  {
229
250
  "name": "ToastProvider",
230
251
  "siblingNames": [
231
- "useToast"
252
+ "useToast",
253
+ "showToast",
254
+ "hideToast",
255
+ "hideAllToasts"
232
256
  ],
233
257
  "category": "organisms",
234
258
  "file": "organisms/ToastProvider.tsx",
@@ -258,6 +282,20 @@
258
282
  "category": "organisms",
259
283
  "file": "organisms/Wizard.tsx",
260
284
  "slug": "wizard"
285
+ },
286
+ {
287
+ "name": "ThemeToggle",
288
+ "siblingNames": [],
289
+ "category": "organisms",
290
+ "file": "organisms/ThemeToggle.tsx",
291
+ "slug": "theme-toggle"
292
+ },
293
+ {
294
+ "name": "NavigationBar",
295
+ "siblingNames": [],
296
+ "category": "organisms",
297
+ "file": "organisms/NavigationBar.tsx",
298
+ "slug": "navigation-bar"
261
299
  }
262
300
  ]
263
301
  }