@stackcraftmx/calcita 0.23.0 → 0.25.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 (359) hide show
  1. package/README.md +111 -22
  2. package/cli/projectFiles.js +5 -3
  3. package/cli/templates/project/README.md.tpl +28 -0
  4. package/cli/templates/project/public/manifest.webmanifest.tpl +6 -6
  5. package/cli/templates/project/src/app/AppProviders.jsx.tpl +9 -4
  6. package/cli/templates/project/src/app/appRouter.jsx.tpl +4 -2
  7. package/cli/templates/project/src/app/appSettings.js.tpl +14 -0
  8. package/cli/templates/project/src/core/AppLogo.jsx.tpl +3 -1
  9. package/cli/templates/project/src/core/index.js.tpl +1 -0
  10. package/cli/templates/project/src/core/publicPaths.js.tpl +24 -0
  11. package/cli/templates/project/vite.config.js.tpl +27 -1
  12. package/dist/DataTable/AuditTrailTable.js +5 -3
  13. package/dist/DataTable/AuditTrailTable.js.map +1 -1
  14. package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +2 -1
  15. package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
  16. package/dist/DataTable/EmptyState.js +2 -2
  17. package/dist/DataTable/EmptyState.js.map +1 -1
  18. package/dist/DataTable/FilterChips.js +2 -2
  19. package/dist/DataTable/FilterChips.js.map +1 -1
  20. package/dist/DataTable/GoToPage.js +4 -3
  21. package/dist/DataTable/GoToPage.js.map +1 -1
  22. package/dist/DataTable/ServerTable.js +153 -83
  23. package/dist/DataTable/ServerTable.js.map +1 -1
  24. package/dist/DataTable/rowSelection.js +61 -0
  25. package/dist/DataTable/rowSelection.js.map +1 -0
  26. package/dist/access/PermissionMatrix.js +3 -2
  27. package/dist/access/PermissionMatrix.js.map +1 -1
  28. package/dist/account/AccountSecuritySection.js +2 -2
  29. package/dist/account/AccountSecuritySection.js.map +1 -1
  30. package/dist/account/ForgotPasswordScreen.js +5 -4
  31. package/dist/account/ForgotPasswordScreen.js.map +1 -1
  32. package/dist/account/ProfileScreen.js +39 -13
  33. package/dist/account/ProfileScreen.js.map +1 -1
  34. package/dist/account/ResetPasswordScreen.js +3 -2
  35. package/dist/account/ResetPasswordScreen.js.map +1 -1
  36. package/dist/account/SignInScreen.js +7 -6
  37. package/dist/account/SignInScreen.js.map +1 -1
  38. package/dist/account/accountConnector.js.map +1 -1
  39. package/dist/account/restAccountConnector.js +2 -1
  40. package/dist/account/restAccountConnector.js.map +1 -1
  41. package/dist/adapters/reactRouterRoutes.js +7 -6
  42. package/dist/adapters/reactRouterRoutes.js.map +1 -1
  43. package/dist/api/createApiClient.js +35 -5
  44. package/dist/api/createApiClient.js.map +1 -1
  45. package/dist/appShell/AppLayout.js +27 -7
  46. package/dist/appShell/AppLayout.js.map +1 -1
  47. package/dist/appShell/AppearanceSettings.js +29 -6
  48. package/dist/appShell/AppearanceSettings.js.map +1 -1
  49. package/dist/appShell/AuthLayout.js +2 -0
  50. package/dist/appShell/AuthLayout.js.map +1 -1
  51. package/dist/appShell/BarActionButton.js +3 -3
  52. package/dist/appShell/BarActionButton.js.map +1 -1
  53. package/dist/appShell/BarActions.js +19 -8
  54. package/dist/appShell/BarActions.js.map +1 -1
  55. package/dist/appShell/BarCapsule.js +19 -4
  56. package/dist/appShell/BarCapsule.js.map +1 -1
  57. package/dist/appShell/BarSearch.js +92 -0
  58. package/dist/appShell/BarSearch.js.map +1 -0
  59. package/dist/appShell/MobileTabBar.js +3 -1
  60. package/dist/appShell/MobileTabBar.js.map +1 -1
  61. package/dist/appShell/shellSizes.js +2 -0
  62. package/dist/appShell/shellSizes.js.map +1 -1
  63. package/dist/auth/PasswordChecklist.js +2 -2
  64. package/dist/auth/PasswordChecklist.js.map +1 -1
  65. package/dist/calendar/EventCalendar.js +16 -5
  66. package/dist/calendar/EventCalendar.js.map +1 -1
  67. package/dist/calendar/EventDetails.js +74 -0
  68. package/dist/calendar/EventDetails.js.map +1 -0
  69. package/dist/calendar/calendarEvents.js +31 -0
  70. package/dist/calendar/calendarEvents.js.map +1 -0
  71. package/dist/calendar.js +8 -1
  72. package/dist/calendar.js.map +1 -1
  73. package/dist/charts.js +10 -1
  74. package/dist/charts.js.map +1 -1
  75. package/dist/datePickers/DateInput.js +3 -2
  76. package/dist/datePickers/DateInput.js.map +1 -1
  77. package/dist/downloads/DownloadManager.js +14 -20
  78. package/dist/downloads/DownloadManager.js.map +1 -1
  79. package/dist/downloads/downloadQueue.js +4 -2
  80. package/dist/downloads/downloadQueue.js.map +1 -1
  81. package/dist/downloads/downloadsMachine.js +4 -1
  82. package/dist/downloads/downloadsMachine.js.map +1 -1
  83. package/dist/downloads/fileTypes.js +22 -0
  84. package/dist/downloads/fileTypes.js.map +1 -0
  85. package/dist/feedback/ConfirmDialog.js +3 -2
  86. package/dist/feedback/ConfirmDialog.js.map +1 -1
  87. package/dist/feedback/ModalWaiting.js +2 -2
  88. package/dist/feedback/ModalWaiting.js.map +1 -1
  89. package/dist/feedback/NewBuildNotice.js +2 -2
  90. package/dist/feedback/NewBuildNotice.js.map +1 -1
  91. package/dist/filepond/FileDropzone.js +5 -3
  92. package/dist/filepond/FileDropzone.js.map +1 -1
  93. package/dist/formatting.js +171 -143
  94. package/dist/formatting.js.map +1 -1
  95. package/dist/forms/FormWizard.js +18 -45
  96. package/dist/forms/FormWizard.js.map +1 -1
  97. package/dist/forms/auth/ChangePasswordForm.js +5 -4
  98. package/dist/forms/auth/ChangePasswordForm.js.map +1 -1
  99. package/dist/forms/auth/ForgotPasswordForm.js +2 -2
  100. package/dist/forms/auth/ForgotPasswordForm.js.map +1 -1
  101. package/dist/forms/auth/SignInForm.js +2 -2
  102. package/dist/forms/auth/SignInForm.js.map +1 -1
  103. package/dist/forms/security/SecondStepForm.js +2 -2
  104. package/dist/forms/security/SecondStepForm.js.map +1 -1
  105. package/dist/forms/security/TwoStepVerificationCard.js +8 -6
  106. package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
  107. package/dist/hookForm/FormWizard.js +59 -0
  108. package/dist/hookForm/FormWizard.js.map +1 -0
  109. package/dist/inbox/InboxLayout.js +44 -8
  110. package/dist/inbox/InboxLayout.js.map +1 -1
  111. package/dist/inbox/MessageList.js +71 -21
  112. package/dist/inbox/MessageList.js.map +1 -1
  113. package/dist/inbox/MessageView.js +80 -29
  114. package/dist/inbox/MessageView.js.map +1 -1
  115. package/dist/inbox/inboxDates.js +4 -3
  116. package/dist/inbox/inboxDates.js.map +1 -1
  117. package/dist/index.js +157 -133
  118. package/dist/index.js.map +1 -1
  119. package/dist/inputs/CatalogInput.js +8 -4
  120. package/dist/inputs/CatalogInput.js.map +1 -1
  121. package/dist/inputs/ColorInput.js +3 -2
  122. package/dist/inputs/ColorInput.js.map +1 -1
  123. package/dist/inputs/FormSection.js +3 -3
  124. package/dist/inputs/FormSection.js.map +1 -1
  125. package/dist/inputs/NumberInput.js +3 -2
  126. package/dist/inputs/NumberInput.js.map +1 -1
  127. package/dist/inputs/SelectInput.js +29 -11
  128. package/dist/inputs/SelectInput.js.map +1 -1
  129. package/dist/inputs/TextInput.js +6 -2
  130. package/dist/inputs/TextInput.js.map +1 -1
  131. package/dist/inputs/WizardLayout.js +54 -0
  132. package/dist/inputs/WizardLayout.js.map +1 -0
  133. package/dist/kanban/KanbanBoard.js +239 -14
  134. package/dist/kanban/KanbanBoard.js.map +1 -1
  135. package/dist/kanban/kanbanCards.js +19 -0
  136. package/dist/kanban/kanbanCards.js.map +1 -0
  137. package/dist/kanban.js +7 -1
  138. package/dist/kanban.js.map +1 -1
  139. package/dist/locales/en.js +74 -1
  140. package/dist/locales/en.js.map +1 -1
  141. package/dist/locales/es.js +74 -1
  142. package/dist/locales/es.js.map +1 -1
  143. package/dist/maps/MapView.js +28 -8
  144. package/dist/maps/MapView.js.map +1 -1
  145. package/dist/observability/usePublishedBuildWatch.js +4 -3
  146. package/dist/observability/usePublishedBuildWatch.js.map +1 -1
  147. package/dist/pwa/InstallAppSetting.js +6 -5
  148. package/dist/pwa/InstallAppSetting.js.map +1 -1
  149. package/dist/pwa/InstallInstructionsDialog.js +2 -2
  150. package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
  151. package/dist/pwa/InstallPromptBanner.js +15 -9
  152. package/dist/pwa/InstallPromptBanner.js.map +1 -1
  153. package/dist/pwa/useInstallPrompt.js +2 -1
  154. package/dist/pwa/useInstallPrompt.js.map +1 -1
  155. package/dist/react-hook-form.js +6 -4
  156. package/dist/react-hook-form.js.map +1 -1
  157. package/dist/security/ActivityLog.js +4 -3
  158. package/dist/security/ActivityLog.js.map +1 -1
  159. package/dist/security/RecoveryCodesDialog.js +4 -3
  160. package/dist/security/RecoveryCodesDialog.js.map +1 -1
  161. package/dist/security/SessionsCard.js +8 -6
  162. package/dist/security/SessionsCard.js.map +1 -1
  163. package/dist/table.js +2 -0
  164. package/dist/table.js.map +1 -1
  165. package/dist/theme/components/MuiDialog.js +13 -0
  166. package/dist/theme/components/MuiDialog.js.map +1 -0
  167. package/dist/theme/createToolkitTheme.js.map +1 -1
  168. package/dist/theme/toolkitComponents.js +2 -0
  169. package/dist/theme/toolkitComponents.js.map +1 -1
  170. package/dist/toolkit/ComponentTexts.js +13 -0
  171. package/dist/toolkit/ComponentTexts.js.map +1 -0
  172. package/dist/toolkit/ToolkitProvider.js +36 -28
  173. package/dist/toolkit/ToolkitProvider.js.map +1 -1
  174. package/dist/toolkit/browserNavigation.js +19 -20
  175. package/dist/toolkit/browserNavigation.js.map +1 -1
  176. package/dist/toolkit/componentTextsContext.js +15 -0
  177. package/dist/toolkit/componentTextsContext.js.map +1 -0
  178. package/dist/toolkit/hashNavigation.js +40 -0
  179. package/dist/toolkit/hashNavigation.js.map +1 -0
  180. package/dist/toolkit/navigationPaths.js +49 -0
  181. package/dist/toolkit/navigationPaths.js.map +1 -0
  182. package/dist/toolkit/toolkitSettings.js +24 -4
  183. package/dist/toolkit/toolkitSettings.js.map +1 -1
  184. package/dist/toolkit/useFormatters.js +12 -0
  185. package/dist/toolkit/useFormatters.js.map +1 -0
  186. package/dist/toolkit/useToolkitTranslation.js +11 -7
  187. package/dist/toolkit/useToolkitTranslation.js.map +1 -1
  188. package/dist/types/DataTable/AuditTrailTable.d.ts +3 -1
  189. package/dist/types/DataTable/EmptyState.d.ts +7 -2
  190. package/dist/types/DataTable/FilterChips.d.ts +3 -1
  191. package/dist/types/DataTable/GoToPage.d.ts +7 -2
  192. package/dist/types/DataTable/SelectionBar.d.ts +32 -0
  193. package/dist/types/DataTable/ServerTable.d.ts +64 -3
  194. package/dist/types/DataTable/index.d.ts +1 -1
  195. package/dist/types/DataTable/rowSelection.d.ts +113 -0
  196. package/dist/types/access/PermissionMatrix.d.ts +3 -1
  197. package/dist/types/account/AccountSecuritySection.d.ts +3 -1
  198. package/dist/types/account/ForgotPasswordScreen.d.ts +3 -1
  199. package/dist/types/account/ProfileScreen.d.ts +31 -1
  200. package/dist/types/account/ResetPasswordScreen.d.ts +3 -1
  201. package/dist/types/account/SignInScreen.d.ts +3 -1
  202. package/dist/types/account/accountConnector.d.ts +7 -2
  203. package/dist/types/adapters/reactRouterRoutes.d.ts +14 -2
  204. package/dist/types/api/createApiClient.d.ts +29 -1
  205. package/dist/types/appShell/AppLayout.d.ts +11 -1
  206. package/dist/types/appShell/AppearanceSettings.d.ts +8 -1
  207. package/dist/types/appShell/BarActionButton.d.ts +2 -2
  208. package/dist/types/appShell/BarActions.d.ts +6 -1
  209. package/dist/types/appShell/BarCapsule.d.ts +8 -0
  210. package/dist/types/appShell/BarSearch.d.ts +24 -0
  211. package/dist/types/appShell/MobileTabBar.d.ts +4 -1
  212. package/dist/types/appShell/shellSizes.d.ts +1 -0
  213. package/dist/types/auth/PasswordChecklist.d.ts +3 -1
  214. package/dist/types/calendar/EventCalendar.d.ts +18 -1
  215. package/dist/types/calendar/EventDetails.d.ts +36 -0
  216. package/dist/types/calendar/calendarEvents.d.ts +27 -0
  217. package/dist/types/calendar.d.ts +2 -0
  218. package/dist/types/charts.d.ts +4 -0
  219. package/dist/types/datePickers/DateInput.d.ts +3 -1
  220. package/dist/types/downloads/DownloadManager.d.ts +7 -1
  221. package/dist/types/downloads/downloadQueue.d.ts +8 -3
  222. package/dist/types/downloads/fileTypes.d.ts +39 -0
  223. package/dist/types/downloads/index.d.ts +1 -0
  224. package/dist/types/feedback/ConfirmDialog.d.ts +3 -1
  225. package/dist/types/feedback/ModalWaiting.d.ts +7 -2
  226. package/dist/types/feedback/NewBuildNotice.d.ts +3 -1
  227. package/dist/types/filepond/FileDropzone.d.ts +3 -1
  228. package/dist/types/formatting.d.ts +45 -90
  229. package/dist/types/forms/FormWizard.d.ts +3 -1
  230. package/dist/types/forms/auth/ChangePasswordForm.d.ts +3 -1
  231. package/dist/types/forms/auth/ForgotPasswordForm.d.ts +3 -1
  232. package/dist/types/forms/auth/SignInForm.d.ts +3 -1
  233. package/dist/types/forms/security/SecondStepForm.d.ts +3 -1
  234. package/dist/types/forms/security/TwoStepVerificationCard.d.ts +3 -1
  235. package/dist/types/hookForm/FormWizard.d.ts +22 -0
  236. package/dist/types/inbox/InboxLayout.d.ts +14 -2
  237. package/dist/types/inbox/MessageList.d.ts +30 -5
  238. package/dist/types/inbox/MessageView.d.ts +35 -3
  239. package/dist/types/inbox/inboxDates.d.ts +6 -1
  240. package/dist/types/index.d.ts +12 -5
  241. package/dist/types/inputs/CatalogInput.d.ts +9 -5
  242. package/dist/types/inputs/ColorInput.d.ts +3 -1
  243. package/dist/types/inputs/FormSection.d.ts +2 -2
  244. package/dist/types/inputs/NumberInput.d.ts +3 -1
  245. package/dist/types/inputs/SelectInput.d.ts +9 -5
  246. package/dist/types/inputs/TextInput.d.ts +3 -1
  247. package/dist/types/inputs/WizardLayout.d.ts +34 -0
  248. package/dist/types/kanban/KanbanBoard.d.ts +61 -6
  249. package/dist/types/kanban/kanbanCards.d.ts +22 -0
  250. package/dist/types/kanban.d.ts +1 -0
  251. package/dist/types/locales/en.d.ts +77 -0
  252. package/dist/types/locales/es.d.ts +77 -0
  253. package/dist/types/maps/MapView.d.ts +12 -3
  254. package/dist/types/observability/usePublishedBuildWatch.d.ts +3 -1
  255. package/dist/types/pwa/InstallAppSetting.d.ts +7 -1
  256. package/dist/types/pwa/InstallInstructionsDialog.d.ts +3 -1
  257. package/dist/types/pwa/InstallPromptBanner.d.ts +10 -1
  258. package/dist/types/pwa/useInstallPrompt.d.ts +3 -2
  259. package/dist/types/react-hook-form.d.ts +1 -0
  260. package/dist/types/security/ActivityLog.d.ts +3 -1
  261. package/dist/types/security/RecoveryCodesDialog.d.ts +6 -1
  262. package/dist/types/security/SessionsCard.d.ts +3 -1
  263. package/dist/types/table.d.ts +3 -1
  264. package/dist/types/theme/components/MuiDialog.d.ts +9 -0
  265. package/dist/types/toolkit/ComponentTexts.d.ts +11 -0
  266. package/dist/types/toolkit/ToolkitProvider.d.ts +18 -2
  267. package/dist/types/toolkit/browserNavigation.d.ts +23 -28
  268. package/dist/types/toolkit/componentTextsContext.d.ts +20 -0
  269. package/dist/types/toolkit/hashNavigation.d.ts +12 -0
  270. package/dist/types/toolkit/navigationPaths.d.ts +43 -0
  271. package/dist/types/toolkit/toolkitSettings.d.ts +49 -4
  272. package/dist/types/toolkit/useFormatters.d.ts +6 -0
  273. package/dist/types/toolkit/useToolkitTranslation.d.ts +2 -1
  274. package/dist/types/ui/actions/ActionsMenu.d.ts +20 -0
  275. package/dist/types/ui/actions/CopyButton.d.ts +3 -1
  276. package/dist/types/ui/actions/DownloadMenu.d.ts +8 -3
  277. package/dist/types/ui/actions/SecretField.d.ts +5 -1
  278. package/dist/types/ui/actions/SelectionActions.d.ts +39 -0
  279. package/dist/types/ui/filters/DateRangeFilter.d.ts +3 -1
  280. package/dist/types/ui/filters/QuickFilters.d.ts +3 -1
  281. package/dist/types/ui/index.d.ts +1 -1
  282. package/dist/types/ui/labels/tagOf.d.ts +13 -0
  283. package/dist/types/ui/layout/CollapsibleSection.d.ts +4 -1
  284. package/dist/types/ui/layout/ContentDialog.d.ts +3 -1
  285. package/dist/types/ui/layout/ErrorPage.d.ts +14 -1
  286. package/dist/types/ui/layout/FullPageLoader.d.ts +7 -1
  287. package/dist/types/ui/layout/PageHeading.d.ts +3 -1
  288. package/dist/types/ui/metrics/BarList.d.ts +7 -1
  289. package/dist/types/ui/metrics/CalendarHeatmap.d.ts +7 -1
  290. package/dist/types/ui/metrics/ChartTooltip.d.ts +9 -3
  291. package/dist/types/ui/metrics/ColumnChart.d.ts +23 -11
  292. package/dist/types/ui/metrics/LineChart.d.ts +41 -0
  293. package/dist/types/ui/metrics/MetricCard.d.ts +9 -1
  294. package/dist/types/ui/metrics/TableToggle.d.ts +9 -2
  295. package/dist/types/ui/metrics/chartParts.d.ts +112 -0
  296. package/dist/types/ui/navigation/BackButton.d.ts +3 -1
  297. package/dist/types/ui/navigation/ColorSchemeToggle.d.ts +8 -2
  298. package/dist/types/ui/navigation/FontSizeToggle.d.ts +7 -2
  299. package/dist/types/ui/navigation/LanguageToggle.d.ts +8 -3
  300. package/dist/types/ui/navigation/PageBreadcrumbs.d.ts +5 -2
  301. package/dist/types/ui/timeline/GroupedTimeline.d.ts +4 -1
  302. package/dist/types/upload/FileDropzone.d.ts +3 -1
  303. package/dist/ui/actions/ActionsMenu.js +51 -0
  304. package/dist/ui/actions/ActionsMenu.js.map +1 -0
  305. package/dist/ui/actions/CopyButton.js +2 -2
  306. package/dist/ui/actions/CopyButton.js.map +1 -1
  307. package/dist/ui/actions/DownloadMenu.js +11 -12
  308. package/dist/ui/actions/DownloadMenu.js.map +1 -1
  309. package/dist/ui/actions/SecretField.js +8 -4
  310. package/dist/ui/actions/SecretField.js.map +1 -1
  311. package/dist/ui/actions/SelectionActions.js +142 -0
  312. package/dist/ui/actions/SelectionActions.js.map +1 -0
  313. package/dist/ui/filters/DateRangeFilter.js +4 -3
  314. package/dist/ui/filters/DateRangeFilter.js.map +1 -1
  315. package/dist/ui/filters/QuickFilters.js +2 -2
  316. package/dist/ui/filters/QuickFilters.js.map +1 -1
  317. package/dist/ui/labels/tagOf.js +7 -0
  318. package/dist/ui/labels/tagOf.js.map +1 -0
  319. package/dist/ui/layout/CollapsibleSection.js +6 -2
  320. package/dist/ui/layout/CollapsibleSection.js.map +1 -1
  321. package/dist/ui/layout/ContentDialog.js +2 -2
  322. package/dist/ui/layout/ContentDialog.js.map +1 -1
  323. package/dist/ui/layout/ErrorPage.js +21 -7
  324. package/dist/ui/layout/ErrorPage.js.map +1 -1
  325. package/dist/ui/layout/FullPageLoader.js +2 -2
  326. package/dist/ui/layout/FullPageLoader.js.map +1 -1
  327. package/dist/ui/layout/PageHeading.js +2 -2
  328. package/dist/ui/layout/PageHeading.js.map +1 -1
  329. package/dist/ui/metrics/BarList.js +18 -13
  330. package/dist/ui/metrics/BarList.js.map +1 -1
  331. package/dist/ui/metrics/CalendarHeatmap.js +11 -6
  332. package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
  333. package/dist/ui/metrics/ChartTooltip.js +1 -1
  334. package/dist/ui/metrics/ChartTooltip.js.map +1 -1
  335. package/dist/ui/metrics/ColumnChart.js +59 -63
  336. package/dist/ui/metrics/ColumnChart.js.map +1 -1
  337. package/dist/ui/metrics/LineChart.js +117 -0
  338. package/dist/ui/metrics/LineChart.js.map +1 -0
  339. package/dist/ui/metrics/MetricCard.js +41 -8
  340. package/dist/ui/metrics/MetricCard.js.map +1 -1
  341. package/dist/ui/metrics/TableToggle.js +4 -2
  342. package/dist/ui/metrics/TableToggle.js.map +1 -1
  343. package/dist/ui/metrics/chartParts.js +93 -0
  344. package/dist/ui/metrics/chartParts.js.map +1 -0
  345. package/dist/ui/navigation/BackButton.js +2 -2
  346. package/dist/ui/navigation/BackButton.js.map +1 -1
  347. package/dist/ui/navigation/ColorSchemeToggle.js +6 -3
  348. package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
  349. package/dist/ui/navigation/FontSizeToggle.js +3 -2
  350. package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
  351. package/dist/ui/navigation/LanguageToggle.js +40 -19
  352. package/dist/ui/navigation/LanguageToggle.js.map +1 -1
  353. package/dist/ui/navigation/PageBreadcrumbs.js +2 -2
  354. package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
  355. package/dist/ui/timeline/GroupedTimeline.js +10 -8
  356. package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
  357. package/dist/upload/FileDropzone.js +5 -3
  358. package/dist/upload/FileDropzone.js.map +1 -1
  359. package/package.json +14 -6
@@ -1 +1 @@
1
- {"version":3,"file":"ProfileScreen.js","sources":["../../src/account/ProfileScreen.jsx"],"sourcesContent":["// Mi perfil, completo: quién está dentro (ProfileHeader con sus perfiles como chips) y tres pestañas:\n// - Sus datos: nombre y teléfono (el correo se ve pero no se cambia aquí); al guardar se vuelve a leer la sesión, así la\n// barra y el menú de la cuenta ya dicen el nombre nuevo;\n// - Seguridad: AccountSecuritySection (contraseña, dos pasos, sesiones y bitácora);\n// - Preferencias: AppearanceSettings (tamaño de letra, idioma y modo de color).\n// La pestaña abierta va en la dirección (?tab=): se puede compartir o recargar sin perderla. Lo del servidor sale del\n// conector de la cuenta.\n//\n// <ProfileScreen />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik} from 'formik';\nimport Stack from '@mui/material/Stack';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\n\nimport AccountSecuritySection from '@/account/AccountSecuritySection.jsx';\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport AppearanceSettings from '@/appShell/AppearanceSettings.jsx';\nimport {toastSuccess} from '@/feedback/toast.jsx';\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport TextInput from '@/inputs/TextInput.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ProfileHeader from '@/ui/layout/ProfileHeader.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\n\n/** Las pestañas, en el orden en que se pintan. */\nexport const PROFILE_TAB = Object.freeze({data: 'data', security: 'security', preferences: 'preferences'});\nconst TAB_PARAM = 'tab';\n// el teléfono: dígitos, con + opcional al inicio (formato internacional); lo largo que permite el plan de numeración\nconst PHONE_PATTERN = /^\\+?\\d{7,15}$/;\n// lo ancho de los formularios: un renglón cómodo, no toda la pantalla\nconst FORM_MAX_WIDTH = 520;\n\n/** Sus datos: nombre y teléfono. */\nfunction ProfileDataSection() {\n const {t} = useToolkitTranslation('account');\n const {connector, session, refreshSession} = useAccount();\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = useMemo(() => Yup.object({\n name: Yup.string().trim().required(),\n phone: Yup.string().trim().matches(PHONE_PATTERN, {message: {key: 'account:profile.data.phoneInvalid'}, excludeEmptyString: true}),\n }), []);\n\n const save = async (values, formikHelpers) => {\n try {\n const response = await connector.updateProfile({name: values.name.trim(), phone: values.phone.trim()});\n await refreshSession();\n toastSuccess(response?.message ?? t('profile.data.saved'));\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <SectionCard title={t('profile.data.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 2}}>{t('profile.data.text')}</Typography>\n <Formik initialValues={{name: session.user.name ?? '', phone: session.user.phone ?? ''}} validationSchema={schema} onSubmit={save}>\n <Form noValidate>\n <Stack spacing={2} sx={{maxWidth: FORM_MAX_WIDTH}}>\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('profile.data.name')} autoComplete=\"name\" />\n {/* el correo se ve con el mismo estilo de los demás campos, pero no se cambia aquí */}\n <TextInput name=\"email\" label={t('profile.data.email')} value={session.user.email} onChange={() => {}}\n helperText={t('profile.data.emailHelp')} disabled />\n <FormTextField name=\"phone\" label={t('profile.data.phone')} type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('profile.data.save')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n </SectionCard>\n );\n}\n\n/**\n * @param {object} props\n * @param {boolean} [props.showColorScheme] el ajuste de claro u oscuro en Preferencias; sin modo oscuro, false\n */\nexport default function ProfileScreen({showColorScheme = true}) {\n const {t} = useToolkitTranslation('account');\n const {session} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {pathname, search} = useLocation();\n const requestedTab = new URLSearchParams(search).get(TAB_PARAM);\n const activeTab = Object.values(PROFILE_TAB).includes(requestedTab) ? requestedTab : PROFILE_TAB.data;\n\n // guard clause: sin sesión no hay perfil que enseñar (el marco de la app ya lleva a entrar)\n if (!session) {\n return null;\n }\n\n return (\n <Stack spacing={2}>\n <ProfileHeader name={session.user.name} email={session.user.email} subtitle={session.user.subtitle} avatarUrl={session.user.avatarUrl}\n chips={session.labels ?? []} />\n <Tabs value={activeTab} onChange={(event, tab) => navigate(`${pathname}?${TAB_PARAM}=${tab}`, {replace: true})}\n aria-label={t('profile.tabs.label')}>\n {Object.values(PROFILE_TAB).map((tab) => <Tab key={tab} value={tab} label={t(`profile.tabs.${tab}`)} />)}\n </Tabs>\n {activeTab === PROFILE_TAB.data && <ProfileDataSection />}\n {activeTab === PROFILE_TAB.security && <AccountSecuritySection />}\n {activeTab === PROFILE_TAB.preferences && (\n <SectionCard title={t('profile.preferences.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 1}}>{t('profile.preferences.text')}</Typography>\n <Stack sx={{maxWidth: FORM_MAX_WIDTH, mx: -1}}>\n <AppearanceSettings showColorScheme={showColorScheme} />\n </Stack>\n </SectionCard>\n )}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ProfileScreen.js","sources":["../../src/account/ProfileScreen.jsx"],"sourcesContent":["// Mi perfil, completo: quién está dentro (ProfileHeader con sus perfiles como chips) y tres pestañas:\n// - Sus datos: nombre y teléfono (el correo se ve pero no se cambia aquí); al guardar se vuelve a leer la sesión, así la\n// barra y el menú de la cuenta ya dicen el nombre nuevo;\n// - Seguridad: AccountSecuritySection (contraseña, dos pasos, sesiones y bitácora);\n// - Preferencias: AppearanceSettings (tamaño de letra, idioma y modo de color).\n// La pestaña abierta va en la dirección (?tab=): se puede compartir o recargar sin perderla. Lo del servidor sale del\n// conector de la cuenta.\n//\n// Se extiende sin tocar el framework (cada proyecto tiene su perfil «especial»):\n// - extraFields: campos propios en «Sus datos» (el área, el número de empleado…), con su valor de inicio sacado del usuario\n// de la sesión, sus reglas de yup y sus campos. Al guardar viajan a connector.updateProfile junto con name y phone: un\n// backend que ya existe recibe lo suyo con sus nombres;\n// - extraTabs: pestañas propias después de las de la casa (facturación, notificaciones…); hiddenTabs esconde las de la casa;\n// - eventos: onTabChange y onProfileSaved (lo guardado y lo que contestó el servidor);\n// - texts: cualquier texto, con la forma de `account` en los textos del paquete.\n//\n// <ProfileScreen />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik} from 'formik';\nimport Stack from '@mui/material/Stack';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport Typography from '@mui/material/Typography';\n\nimport AccountSecuritySection from '@/account/AccountSecuritySection.jsx';\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport AppearanceSettings from '@/appShell/AppearanceSettings.jsx';\nimport {toastSuccess} from '@/feedback/toast.jsx';\nimport FormProblemAlert from '@/forms/FormProblemAlert.jsx';\nimport FormTextField from '@/forms/FormTextField.jsx';\nimport SubmitButton from '@/forms/SubmitButton.jsx';\nimport {applyServerProblem} from '@/forms/applyServerProblem.js';\nimport {Yup} from '@/forms/validation.js';\nimport TextInput from '@/inputs/TextInput.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ProfileHeader from '@/ui/layout/ProfileHeader.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\n\n/** Las pestañas, en el orden en que se pintan. */\nexport const PROFILE_TAB = Object.freeze({data: 'data', security: 'security', preferences: 'preferences'});\nconst TAB_PARAM = 'tab';\n// el teléfono: dígitos, con + opcional al inicio (formato internacional); lo largo que permite el plan de numeración\nconst PHONE_PATTERN = /^\\+?\\d{7,15}$/;\n// lo ancho de los formularios: un renglón cómodo, no toda la pantalla\nconst FORM_MAX_WIDTH = 520;\n\n/**\n * Sus datos: nombre, teléfono y los campos propios del proyecto (extraFields).\n *\n * @param {object} props\n * @param {object} [props.extraFields]\n * @param {Function} [props.onProfileSaved]\n * @param {object} [props.texts]\n */\nfunction ProfileDataSection({extraFields, onProfileSaved, texts}) {\n const {t} = useToolkitTranslation('account', texts);\n const {connector, session, refreshSession} = useAccount();\n const extraRules = extraFields?.rules;\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo. Las reglas propias se suman a las de la casa\n const schema = useMemo(() => Yup.object({\n name: Yup.string().trim().required(),\n phone: Yup.string().trim().matches(PHONE_PATTERN, {message: {key: 'account:profile.data.phoneInvalid'}, excludeEmptyString: true}),\n ...extraRules,\n }), [extraRules]);\n const initialValues = {name: session.user.name ?? '', phone: session.user.phone ?? '', ...extraFields?.initialValues?.(session.user)};\n\n const save = async (values, formikHelpers) => {\n // lo de la casa limpio de espacios; lo propio, tal como lo dejaron sus campos\n const profile = {...values, name: values.name.trim(), phone: values.phone.trim()};\n try {\n const response = await connector.updateProfile(profile);\n await refreshSession();\n toastSuccess(response?.message ?? t('profile.data.saved'));\n onProfileSaved?.({values: profile, response});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <SectionCard title={t('profile.data.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 2}}>{t('profile.data.text')}</Typography>\n <Formik initialValues={initialValues} validationSchema={schema} onSubmit={save}>\n <Form noValidate>\n <Stack spacing={2} sx={{maxWidth: FORM_MAX_WIDTH}}>\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('profile.data.name')} autoComplete=\"name\" />\n {/* el correo se ve con el mismo estilo de los demás campos, pero no se cambia aquí */}\n <TextInput name=\"email\" label={t('profile.data.email')} value={session.user.email} onChange={() => {}}\n helperText={t('profile.data.emailHelp')} disabled />\n <FormTextField name=\"phone\" label={t('profile.data.phone')} type=\"tel\" autoComplete=\"tel\" inputMode=\"tel\" />\n {extraFields?.fields}\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('profile.data.save')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n </SectionCard>\n );\n}\n\n/** @typedef {import('@/account/accountConnector.js').AccountUser} AccountUser */\n\n/**\n * @param {object} props\n * @param {boolean} [props.showColorScheme] el ajuste de claro u oscuro en Preferencias; sin modo oscuro, false\n * @param {{initialValues?: (user: AccountUser & Record<string, any>) => Record<string, any>, rules?: Record<string, any>, fields: import('react').ReactNode}} [props.extraFields]\n * campos propios en «Sus datos», debajo del teléfono: sus valores de inicio salen del usuario de la sesión (lo que su\n * conector ponga en session.user), sus reglas de yup y sus campos (FormTextField, FormSelectField… de /forms). Al guardar\n * viajan a connector.updateProfile junto con name y phone\n * @param {Array<{key: string, label: string, content: import('react').ReactNode}>} [props.extraTabs] pestañas propias,\n * después de las de la casa; label ya traducido\n * @param {Array<'data'|'security'|'preferences'>} [props.hiddenTabs] pestañas de la casa que no se ven (PROFILE_TAB)\n * @param {(tab: string) => void} [props.onTabChange] cada que se cambia de pestaña\n * @param {(saved: {values: Record<string, any>, response: unknown}) => void} [props.onProfileSaved] después de guardar «Sus datos»\n * @param {object} [props.texts] los textos que cambian, con la forma de `account` en los textos del paquete:\n * {profile: {tabs: {data: 'Mis datos'}}}\n */\nexport default function ProfileScreen({\n showColorScheme = true, extraFields, extraTabs = [], hiddenTabs = [], onTabChange, onProfileSaved, texts,\n}) {\n const {t} = useToolkitTranslation('account', texts);\n const {session} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {pathname, search} = useLocation();\n // las pestañas que se ven: las de la casa que no se escondieron y luego las del proyecto\n const tabs = [\n ...Object.values(PROFILE_TAB).filter((tab) => !hiddenTabs.includes(tab)).map((tab) => ({key: tab, label: t(`profile.tabs.${tab}`)})),\n ...extraTabs.map(({key, label}) => ({key, label})),\n ];\n const requestedTab = new URLSearchParams(search).get(TAB_PARAM);\n const activeTab = tabs.some((tab) => tab.key === requestedTab) ? requestedTab : tabs[0]?.key;\n const activeExtraTab = extraTabs.find((tab) => tab.key === activeTab);\n const changeTab = (tab) => {\n navigate(`${pathname}?${TAB_PARAM}=${tab}`, {replace: true});\n onTabChange?.(tab);\n };\n\n // guard clause: sin sesión no hay perfil que enseñar (el marco de la app ya lleva a entrar)\n if (!session) {\n return null;\n }\n\n return (\n <Stack spacing={2}>\n <ProfileHeader name={session.user.name} email={session.user.email} subtitle={session.user.subtitle} avatarUrl={session.user.avatarUrl}\n chips={session.labels ?? []} />\n <Tabs value={activeTab} onChange={(event, tab) => changeTab(tab)} aria-label={t('profile.tabs.label')}\n variant=\"scrollable\" allowScrollButtonsMobile>\n {tabs.map((tab) => <Tab key={tab.key} value={tab.key} label={tab.label} />)}\n </Tabs>\n {activeTab === PROFILE_TAB.data && <ProfileDataSection extraFields={extraFields} onProfileSaved={onProfileSaved} texts={texts} />}\n {activeTab === PROFILE_TAB.security && <AccountSecuritySection />}\n {activeTab === PROFILE_TAB.preferences && (\n <SectionCard title={t('profile.preferences.title')}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{mb: 1}}>{t('profile.preferences.text')}</Typography>\n <Stack sx={{maxWidth: FORM_MAX_WIDTH, mx: -1}}>\n <AppearanceSettings showColorScheme={showColorScheme} />\n </Stack>\n </SectionCard>\n )}\n {activeExtraTab?.content}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -16,9 +16,10 @@ function ResetPasswordScreen({
16
16
  logo,
17
17
  brandName,
18
18
  showLanguageToggle = true,
19
- showColorSchemeToggle = true
19
+ showColorSchemeToggle = true,
20
+ texts
20
21
  }) {
21
- const { t } = useToolkitTranslation("account");
22
+ const { t } = useToolkitTranslation("account", texts);
22
23
  const { connector } = useAccount();
23
24
  const { useLocation, Link } = useToolkitNavigation();
24
25
  const { search } = useLocation();
@@ -1 +1 @@
1
- {"version":3,"file":"ResetPasswordScreen.js","sources":["../../src/account/ResetPasswordScreen.jsx"],"sourcesContent":["// Elegir una contraseña nueva desde el enlace del correo, completa: ChangePasswordForm sin la contraseña actual (quien llega\n// aquí la olvidó). El enlace trae su clave (?token=) y el servidor la revisa al guardar (connector.resetPassword). Sin\n// clave en la dirección, se dice que el enlace no sirve y cómo pedir otro.\n//\n// <ResetPasswordScreen signInPath=\"/sign-in\" forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport ChangePasswordForm from '@/forms/auth/ChangePasswordForm.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} props.signInPath a dónde ir al terminar\n * @param {string} [props.forgotPasswordPath] para pedir otro enlace si este no sirve\n * @param {string} [props.tokenParam] cómo se llama la clave en la dirección; por omisión 'token'\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName]\n * @param {boolean} [props.showLanguageToggle]\n * @param {boolean} [props.showColorSchemeToggle]\n */\nexport default function ResetPasswordScreen({\n signInPath, forgotPasswordPath, tokenParam = 'token', logo, brandName, showLanguageToggle = true, showColorSchemeToggle = true,\n}) {\n const {t} = useToolkitTranslation('account');\n const {connector} = useAccount();\n const {useLocation, Link} = useToolkitNavigation();\n const {search} = useLocation();\n const [isDone, setIsDone] = useState(false);\n const token = new URLSearchParams(search).get(tokenParam);\n\n const save = async ({newPassword}) => {\n await connector.resetPassword({token, newPassword});\n setIsDone(true);\n };\n\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('reset.title')} description={t('reset.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n {!token && (\n <Stack spacing={2}>\n <Alert severity=\"error\">{t('reset.missingToken')}</Alert>\n {forgotPasswordPath && <Button component={Link} to={forgotPasswordPath} variant=\"contained\">{t('reset.askAgain')}</Button>}\n </Stack>\n )}\n {token && isDone && (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">{t('reset.done')}</Alert>\n <Button component={Link} to={signInPath} variant=\"contained\">{t('reset.goToSignIn')}</Button>\n </Stack>\n )}\n {token && !isDone && <ChangePasswordForm requireCurrentPassword={false} onSubmit={save} />}\n </AuthLayout>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA4BA,SAAwB,oBAAoB;AAAA,EACxC;AAAA,EAAY;AAAA,EAAoB,aAAa;AAAA,EAAS;AAAA,EAAM;AAAA,EAAW,qBAAqB;AAAA,EAAM,wBAAwB;AAC9H,GAAG;AACC,QAAM,EAAC,EAAA,IAAK,sBAAsB,SAAS;AAC3C,QAAM,EAAC,UAAA,IAAa,WAAA;AACpB,QAAM,EAAC,aAAa,KAAA,IAAQ,qBAAA;AAC5B,QAAM,EAAC,OAAA,IAAU,YAAA;AACjB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,QAAQ,IAAI,gBAAgB,MAAM,EAAE,IAAI,UAAU;AAExD,QAAM,OAAO,OAAO,EAAC,kBAAiB;AAClC,UAAM,UAAU,cAAc,EAAC,OAAO,aAAY;AAClD,cAAU,IAAI;AAAA,EAClB;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,aAAa;AAAA,MAAG,aAAa,EAAE,gBAAgB;AAAA,MAC1F;AAAA,MAAwC;AAAA,MAC/C,UAAA;AAAA,QAAA,CAAC,SACE,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,UAAS,SAAS,UAAA,EAAE,oBAAoB,GAAE;AAAA,UAChD,sBAAsB,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,oBAAoB,SAAQ,aAAa,UAAA,EAAE,gBAAgB,EAAA,CAAE;AAAA,QAAA,GACrH;AAAA,QAEH,SAAS,UACN,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,YAAY,GAAE;AAAA,UACzD,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,YAAY,SAAQ,aAAa,UAAA,EAAE,kBAAkB,EAAA,CAAE;AAAA,QAAA,GACxF;AAAA,QAEH,SAAS,CAAC,UAAU,oBAAC,sBAAmB,wBAAwB,OAAO,UAAU,KAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGpG;"}
1
+ {"version":3,"file":"ResetPasswordScreen.js","sources":["../../src/account/ResetPasswordScreen.jsx"],"sourcesContent":["// Elegir una contraseña nueva desde el enlace del correo, completa: ChangePasswordForm sin la contraseña actual (quien llega\n// aquí la olvidó). El enlace trae su clave (?token=) y el servidor la revisa al guardar (connector.resetPassword). Sin\n// clave en la dirección, se dice que el enlace no sirve y cómo pedir otro.\n//\n// <ResetPasswordScreen signInPath=\"/sign-in\" forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Stack from '@mui/material/Stack';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport ChangePasswordForm from '@/forms/auth/ChangePasswordForm.jsx';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} props.signInPath a dónde ir al terminar\n * @param {string} [props.forgotPasswordPath] para pedir otro enlace si este no sirve\n * @param {string} [props.tokenParam] cómo se llama la clave en la dirección; por omisión 'token'\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName]\n * @param {boolean} [props.showLanguageToggle]\n * @param {boolean} [props.showColorSchemeToggle]\n * @param {object} [props.texts] los textos que cambian, con la forma de `account` en los textos del paquete\n */\nexport default function ResetPasswordScreen({\n signInPath, forgotPasswordPath, tokenParam = 'token', logo, brandName, showLanguageToggle = true, showColorSchemeToggle = true, texts,\n}) {\n const {t} = useToolkitTranslation('account', texts);\n const {connector} = useAccount();\n const {useLocation, Link} = useToolkitNavigation();\n const {search} = useLocation();\n const [isDone, setIsDone] = useState(false);\n const token = new URLSearchParams(search).get(tokenParam);\n\n const save = async ({newPassword}) => {\n await connector.resetPassword({token, newPassword});\n setIsDone(true);\n };\n\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('reset.title')} description={t('reset.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n {!token && (\n <Stack spacing={2}>\n <Alert severity=\"error\">{t('reset.missingToken')}</Alert>\n {forgotPasswordPath && <Button component={Link} to={forgotPasswordPath} variant=\"contained\">{t('reset.askAgain')}</Button>}\n </Stack>\n )}\n {token && isDone && (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">{t('reset.done')}</Alert>\n <Button component={Link} to={signInPath} variant=\"contained\">{t('reset.goToSignIn')}</Button>\n </Stack>\n )}\n {token && !isDone && <ChangePasswordForm requireCurrentPassword={false} onSubmit={save} />}\n </AuthLayout>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA6BA,SAAwB,oBAAoB;AAAA,EACxC;AAAA,EAAY;AAAA,EAAoB,aAAa;AAAA,EAAS;AAAA,EAAM;AAAA,EAAW,qBAAqB;AAAA,EAAM,wBAAwB;AAAA,EAAM;AACpI,GAAG;AACC,QAAM,EAAC,EAAA,IAAK,sBAAsB,WAAW,KAAK;AAClD,QAAM,EAAC,UAAA,IAAa,WAAA;AACpB,QAAM,EAAC,aAAa,KAAA,IAAQ,qBAAA;AAC5B,QAAM,EAAC,OAAA,IAAU,YAAA;AACjB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,QAAQ,IAAI,gBAAgB,MAAM,EAAE,IAAI,UAAU;AAExD,QAAM,OAAO,OAAO,EAAC,kBAAiB;AAClC,UAAM,UAAU,cAAc,EAAC,OAAO,aAAY;AAClD,cAAU,IAAI;AAAA,EAClB;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,aAAa;AAAA,MAAG,aAAa,EAAE,gBAAgB;AAAA,MAC1F;AAAA,MAAwC;AAAA,MAC/C,UAAA;AAAA,QAAA,CAAC,SACE,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAM,UAAS,SAAS,UAAA,EAAE,oBAAoB,GAAE;AAAA,UAChD,sBAAsB,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,oBAAoB,SAAQ,aAAa,UAAA,EAAE,gBAAgB,EAAA,CAAE;AAAA,QAAA,GACrH;AAAA,QAEH,SAAS,UACN,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,UAAA,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,YAAY,GAAE;AAAA,UACzD,oBAAC,QAAA,EAAO,WAAW,MAAM,IAAI,YAAY,SAAQ,aAAa,UAAA,EAAE,kBAAkB,EAAA,CAAE;AAAA,QAAA,GACxF;AAAA,QAEH,SAAS,CAAC,UAAU,oBAAC,sBAAmB,wBAAwB,OAAO,UAAU,KAAA,CAAM;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGpG;"}
@@ -11,8 +11,9 @@ import { readRedirectParam } from "../navigation.js";
11
11
  import { signWithPasskey, passkeysSupported } from "../security/passkeys.js";
12
12
  import { useToolkitNavigation } from "../toolkit/toolkitContext.js";
13
13
  import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
14
- function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true }) {
15
- const { t } = useToolkitTranslation("auth");
14
+ import ComponentTexts from "../toolkit/ComponentTexts.js";
15
+ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true, texts }) {
16
+ const { t } = useToolkitTranslation("auth", texts);
16
17
  const { connector, session, refreshSession } = useAccount();
17
18
  const { useNavigate, useLocation } = useToolkitNavigation();
18
19
  const navigate = useNavigate();
@@ -50,7 +51,7 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
50
51
  }
51
52
  if (secondStep) {
52
53
  const showsDemoCode = typeof connector.demoTotpCode === "function" && secondStep.methods.includes(SECOND_STEP_METHOD.totp);
53
- return /* @__PURE__ */ jsx(
54
+ return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "auth", texts, children: /* @__PURE__ */ jsx(
54
55
  AuthLayout,
55
56
  {
56
57
  logo,
@@ -76,9 +77,9 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
76
77
  }
77
78
  )
78
79
  }
79
- );
80
+ ) });
80
81
  }
81
- return /* @__PURE__ */ jsxs(
82
+ return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "auth", texts, children: /* @__PURE__ */ jsxs(
82
83
  AuthLayout,
83
84
  {
84
85
  logo,
@@ -93,7 +94,7 @@ function SignInScreen({ homePath = "/", forgotPasswordPath, logo, brandName, foo
93
94
  /* @__PURE__ */ jsx(SignInForm, { onSubmit: signIn, forgotPasswordPath })
94
95
  ]
95
96
  }
96
- );
97
+ ) });
97
98
  }
98
99
  export {
99
100
  SignInScreen as default
@@ -1 +1 @@
1
- {"version":3,"file":"SignInScreen.js","sources":["../../src/account/SignInScreen.jsx"],"sourcesContent":["// Entrar, completo: la contraseña (SignInForm) y, si la cuenta tiene verificación en dos pasos, el segundo paso\n// (SecondStepForm: la llave de acceso, el código de la app o uno de recuperación). Al terminar se vuelve a leer la sesión y\n// se va a la pantalla que se quería abrir (readRedirectParam: solo si es de esta app) o al inicio.\n//\n// Lo del servidor sale del conector de la cuenta (useAccount): en el proyecto la pantalla solo se pone en su ruta.\n//\n// <SignInScreen forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// footer va abajo de la tarjeta. Si es una función, recibe signIn para entrar desde un botón propio (p. ej. «Entrar con la\n// cuenta de ejemplo»), con el mismo segundo paso; si falla, el motivo sale arriba del formulario.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useState} from 'react';\nimport Alert from '@mui/material/Alert';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport DemoTotpCodeButton from '@/account/DemoTotpCodeButton.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport SignInForm from '@/forms/auth/SignInForm.jsx';\nimport SecondStepForm, {SECOND_STEP_METHOD} from '@/forms/security/SecondStepForm.jsx';\nimport {readRedirectParam} from '@/navigation.js';\nimport {passkeysSupported, signWithPasskey} from '@/security/passkeys.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/**\n * @param {object} props\n * @param {string} [props.homePath] a dónde ir si no se quería abrir otra pantalla; por omisión '/'\n * @param {string} [props.forgotPasswordPath] sin ella no sale «¿Olvidó su contraseña?»\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName] por omisión el appName de ToolkitProvider\n * @param {import('react').ReactNode | ((helpers: {signIn: (credentials: {email: string, password: string, remember?: boolean}) => Promise<void>}) => import('react').ReactNode)} [props.footer]\n * @param {boolean} [props.showLanguageToggle] por omisión sí\n * @param {boolean} [props.showColorSchemeToggle] por omisión sí\n */\nexport default function SignInScreen({homePath = '/', forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true}) {\n const {t} = useToolkitTranslation('auth');\n const {connector, session, refreshSession} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {search} = useLocation();\n // con verificación en dos pasos: los métodos del segundo paso (la contraseña ya fue correcta)\n const [secondStep, setSecondStep] = useState(null);\n const [notice, setNotice] = useState(null);\n const destination = readRedirectParam(search) ?? homePath;\n\n // ya hay sesión (acaba de entrar, o abrió esta pantalla estando dentro): a donde iba\n useEffect(() => {\n if (session) {\n navigate(destination, {replace: true});\n }\n }, [session, destination, navigate]);\n\n // después de la contraseña: el segundo paso si lo pide el servidor; si no, ya está dentro\n const signIn = async (credentials) => {\n setNotice(null);\n const result = await connector.signIn({remember: false, ...credentials});\n if (result?.secondStep) {\n setSecondStep(result.secondStep);\n return;\n }\n await refreshSession();\n };\n // desde un botón propio del footer: el error no tiene formulario donde salir, sale arriba\n const signInFromFooter = async (credentials) => {\n try {\n await signIn(credentials);\n } catch (error) {\n setNotice(error?.message ?? null);\n }\n };\n // se acabaron los intentos o se venció el tiempo: de regreso a la contraseña, con el motivo\n const backToPassword = (message) => {\n setSecondStep(null);\n setNotice(message);\n };\n\n if (session) {\n return null;\n }\n\n if (secondStep) {\n const showsDemoCode = typeof connector.demoTotpCode === 'function' && secondStep.methods.includes(SECOND_STEP_METHOD.totp);\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('security:twoStep.title')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n <SecondStepForm methods={secondStep.methods} canUsePasskeys={passkeysSupported()}\n onUsePasskey={async () => {\n await connector.signInWithPasskey(await signWithPasskey(await connector.secondStepPasskeyOptions()));\n await refreshSession();\n }}\n onSubmitCode={async ({method, code}) => {\n await connector.signInSecondStep({method, code});\n await refreshSession();\n }}\n onCancel={backToPassword}\n footer={showsDemoCode && <DemoTotpCodeButton loadCode={connector.demoTotpCode} />} />\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout logo={logo} brandName={brandName} title={t('signIn.title')} description={t('signIn.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}\n footer={typeof footer === 'function' ? footer({signIn: signInFromFooter}) : footer}>\n {notice && <Alert severity=\"warning\" sx={{mb: 2}}>{notice}</Alert>}\n <SignInForm onSubmit={signIn} forgotPasswordPath={forgotPasswordPath} />\n </AuthLayout>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;AAmCA,SAAwB,aAAa,EAAC,WAAW,KAAK,oBAAoB,MAAM,WAAW,QAAQ,qBAAqB,MAAM,wBAAwB,KAAA,GAAO;AACzJ,QAAM,EAAC,EAAA,IAAK,sBAAsB,MAAM;AACxC,QAAM,EAAC,WAAW,SAAS,eAAA,IAAkB,WAAA;AAC7C,QAAM,EAAC,aAAa,YAAA,IAAe,qBAAA;AACnC,QAAM,WAAW,YAAA;AACjB,QAAM,EAAC,OAAA,IAAU,YAAA;AAEjB,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,IAAI;AACzC,QAAM,cAAc,kBAAkB,MAAM,KAAK;AAGjD,YAAU,MAAM;AACZ,QAAI,SAAS;AACT,eAAS,aAAa,EAAC,SAAS,KAAA,CAAK;AAAA,IACzC;AAAA,EACJ,GAAG,CAAC,SAAS,aAAa,QAAQ,CAAC;AAGnC,QAAM,SAAS,OAAO,gBAAgB;AAClC,cAAU,IAAI;AACd,UAAM,SAAS,MAAM,UAAU,OAAO,EAAC,UAAU,OAAO,GAAG,aAAY;AACvE,QAAI,QAAQ,YAAY;AACpB,oBAAc,OAAO,UAAU;AAC/B;AAAA,IACJ;AACA,UAAM,eAAA;AAAA,EACV;AAEA,QAAM,mBAAmB,OAAO,gBAAgB;AAC5C,QAAI;AACA,YAAM,OAAO,WAAW;AAAA,IAC5B,SAAS,OAAO;AACZ,gBAAU,OAAO,WAAW,IAAI;AAAA,IACpC;AAAA,EACJ;AAEA,QAAM,iBAAiB,CAAC,YAAY;AAChC,kBAAc,IAAI;AAClB,cAAU,OAAO;AAAA,EACrB;AAEA,MAAI,SAAS;AACT,WAAO;AAAA,EACX;AAEA,MAAI,YAAY;AACZ,UAAM,gBAAgB,OAAO,UAAU,iBAAiB,cAAc,WAAW,QAAQ,SAAS,mBAAmB,IAAI;AACzH,WACI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAW;AAAA,QAAY;AAAA,QAAsB,OAAO,EAAE,wBAAwB;AAAA,QACnE;AAAA,QAAwC;AAAA,QAChD,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YAAe,SAAS,WAAW;AAAA,YAAS,gBAAgB,kBAAA;AAAA,YAC7C,cAAc,YAAY;AACtB,oBAAM,UAAU,kBAAkB,MAAM,gBAAgB,MAAM,UAAU,yBAAA,CAA0B,CAAC;AACnG,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,cAAc,OAAO,EAAC,QAAQ,WAAU;AACpC,oBAAM,UAAU,iBAAiB,EAAC,QAAQ,MAAK;AAC/C,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,QAAQ,iBAAiB,oBAAC,oBAAA,EAAmB,UAAU,UAAU,aAAA,CAAc;AAAA,UAAA;AAAA,QAAA;AAAA,MAAI;AAAA,IAAA;AAAA,EAG/G;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,cAAc;AAAA,MAAG,aAAa,EAAE,iBAAiB;AAAA,MAC5F;AAAA,MAAwC;AAAA,MACxC,QAAQ,OAAO,WAAW,aAAa,OAAO,EAAC,QAAQ,iBAAA,CAAiB,IAAI;AAAA,MACnF,UAAA;AAAA,QAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,IAAI,EAAC,IAAI,EAAA,GAAK,UAAA,OAAA,CAAO;AAAA,QAC1D,oBAAC,YAAA,EAAW,UAAU,QAAQ,mBAAA,CAAwC;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGlF;"}
1
+ {"version":3,"file":"SignInScreen.js","sources":["../../src/account/SignInScreen.jsx"],"sourcesContent":["// Entrar, completo: la contraseña (SignInForm) y, si la cuenta tiene verificación en dos pasos, el segundo paso\n// (SecondStepForm: la llave de acceso, el código de la app o uno de recuperación). Al terminar se vuelve a leer la sesión y\n// se va a la pantalla que se quería abrir (readRedirectParam: solo si es de esta app) o al inicio.\n//\n// Lo del servidor sale del conector de la cuenta (useAccount): en el proyecto la pantalla solo se pone en su ruta.\n//\n// <SignInScreen forgotPasswordPath=\"/forgot-password\" logo={<AppLogo />} />\n//\n// footer va abajo de la tarjeta. Si es una función, recibe signIn para entrar desde un botón propio (p. ej. «Entrar con la\n// cuenta de ejemplo»), con el mismo segundo paso; si falla, el motivo sale arriba del formulario.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useState} from 'react';\nimport Alert from '@mui/material/Alert';\n\nimport {useAccount} from '@/account/AccountProvider.jsx';\nimport DemoTotpCodeButton from '@/account/DemoTotpCodeButton.jsx';\nimport {AuthLayout} from '@/appShell/AuthLayout.jsx';\nimport SignInForm from '@/forms/auth/SignInForm.jsx';\nimport SecondStepForm, {SECOND_STEP_METHOD} from '@/forms/security/SecondStepForm.jsx';\nimport {readRedirectParam} from '@/navigation.js';\nimport {passkeysSupported, signWithPasskey} from '@/security/passkeys.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n/**\n * @param {object} props\n * @param {string} [props.homePath] a dónde ir si no se quería abrir otra pantalla; por omisión '/'\n * @param {string} [props.forgotPasswordPath] sin ella no sale «¿Olvidó su contraseña?»\n * @param {import('react').ReactNode} [props.logo]\n * @param {string} [props.brandName] por omisión el appName de ToolkitProvider\n * @param {import('react').ReactNode | ((helpers: {signIn: (credentials: {email: string, password: string, remember?: boolean}) => Promise<void>}) => import('react').ReactNode)} [props.footer]\n * @param {boolean} [props.showLanguageToggle] por omisión sí\n * @param {boolean} [props.showColorSchemeToggle] por omisión sí\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInScreen({homePath = '/', forgotPasswordPath, logo, brandName, footer, showLanguageToggle = true, showColorSchemeToggle = true, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const {connector, session, refreshSession} = useAccount();\n const {useNavigate, useLocation} = useToolkitNavigation();\n const navigate = useNavigate();\n const {search} = useLocation();\n // con verificación en dos pasos: los métodos del segundo paso (la contraseña ya fue correcta)\n const [secondStep, setSecondStep] = useState(null);\n const [notice, setNotice] = useState(null);\n const destination = readRedirectParam(search) ?? homePath;\n\n // ya hay sesión (acaba de entrar, o abrió esta pantalla estando dentro): a donde iba\n useEffect(() => {\n if (session) {\n navigate(destination, {replace: true});\n }\n }, [session, destination, navigate]);\n\n // después de la contraseña: el segundo paso si lo pide el servidor; si no, ya está dentro\n const signIn = async (credentials) => {\n setNotice(null);\n const result = await connector.signIn({remember: false, ...credentials});\n if (result?.secondStep) {\n setSecondStep(result.secondStep);\n return;\n }\n await refreshSession();\n };\n // desde un botón propio del footer: el error no tiene formulario donde salir, sale arriba\n const signInFromFooter = async (credentials) => {\n try {\n await signIn(credentials);\n } catch (error) {\n setNotice(error?.message ?? null);\n }\n };\n // se acabaron los intentos o se venció el tiempo: de regreso a la contraseña, con el motivo\n const backToPassword = (message) => {\n setSecondStep(null);\n setNotice(message);\n };\n\n if (session) {\n return null;\n }\n\n if (secondStep) {\n const showsDemoCode = typeof connector.demoTotpCode === 'function' && secondStep.methods.includes(SECOND_STEP_METHOD.totp);\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <AuthLayout logo={logo} brandName={brandName} title={t('security:twoStep.title')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}>\n <SecondStepForm methods={secondStep.methods} canUsePasskeys={passkeysSupported()}\n onUsePasskey={async () => {\n await connector.signInWithPasskey(await signWithPasskey(await connector.secondStepPasskeyOptions()));\n await refreshSession();\n }}\n onSubmitCode={async ({method, code}) => {\n await connector.signInSecondStep({method, code});\n await refreshSession();\n }}\n onCancel={backToPassword}\n footer={showsDemoCode && <DemoTotpCodeButton loadCode={connector.demoTotpCode} />} />\n </AuthLayout>\n </ComponentTexts>\n );\n }\n\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <AuthLayout logo={logo} brandName={brandName} title={t('signIn.title')} description={t('signIn.subtitle')}\n showLanguageToggle={showLanguageToggle} showColorSchemeToggle={showColorSchemeToggle}\n footer={typeof footer === 'function' ? footer({signIn: signInFromFooter}) : footer}>\n {notice && <Alert severity=\"warning\" sx={{mb: 2}}>{notice}</Alert>}\n <SignInForm onSubmit={signIn} forgotPasswordPath={forgotPasswordPath} />\n </AuthLayout>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAqCA,SAAwB,aAAa,EAAC,WAAW,KAAK,oBAAoB,MAAM,WAAW,QAAQ,qBAAqB,MAAM,wBAAwB,MAAM,SAAQ;AAChK,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,WAAW,SAAS,eAAA,IAAkB,WAAA;AAC7C,QAAM,EAAC,aAAa,YAAA,IAAe,qBAAA;AACnC,QAAM,WAAW,YAAA;AACjB,QAAM,EAAC,OAAA,IAAU,YAAA;AAEjB,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,IAAI;AACzC,QAAM,cAAc,kBAAkB,MAAM,KAAK;AAGjD,YAAU,MAAM;AACZ,QAAI,SAAS;AACT,eAAS,aAAa,EAAC,SAAS,KAAA,CAAK;AAAA,IACzC;AAAA,EACJ,GAAG,CAAC,SAAS,aAAa,QAAQ,CAAC;AAGnC,QAAM,SAAS,OAAO,gBAAgB;AAClC,cAAU,IAAI;AACd,UAAM,SAAS,MAAM,UAAU,OAAO,EAAC,UAAU,OAAO,GAAG,aAAY;AACvE,QAAI,QAAQ,YAAY;AACpB,oBAAc,OAAO,UAAU;AAC/B;AAAA,IACJ;AACA,UAAM,eAAA;AAAA,EACV;AAEA,QAAM,mBAAmB,OAAO,gBAAgB;AAC5C,QAAI;AACA,YAAM,OAAO,WAAW;AAAA,IAC5B,SAAS,OAAO;AACZ,gBAAU,OAAO,WAAW,IAAI;AAAA,IACpC;AAAA,EACJ;AAEA,QAAM,iBAAiB,CAAC,YAAY;AAChC,kBAAc,IAAI;AAClB,cAAU,OAAO;AAAA,EACrB;AAEA,MAAI,SAAS;AACT,WAAO;AAAA,EACX;AAEA,MAAI,YAAY;AACZ,UAAM,gBAAgB,OAAO,UAAU,iBAAiB,cAAc,WAAW,QAAQ,SAAS,mBAAmB,IAAI;AACzH,WACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAW;AAAA,QAAY;AAAA,QAAsB,OAAO,EAAE,wBAAwB;AAAA,QACnE;AAAA,QAAwC;AAAA,QAChD,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YAAe,SAAS,WAAW;AAAA,YAAS,gBAAgB,kBAAA;AAAA,YAC7C,cAAc,YAAY;AACtB,oBAAM,UAAU,kBAAkB,MAAM,gBAAgB,MAAM,UAAU,yBAAA,CAA0B,CAAC;AACnG,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,cAAc,OAAO,EAAC,QAAQ,WAAU;AACpC,oBAAM,UAAU,iBAAiB,EAAC,QAAQ,MAAK;AAC/C,oBAAM,eAAA;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,QAAQ,iBAAiB,oBAAC,oBAAA,EAAmB,UAAU,UAAU,aAAA,CAAc;AAAA,UAAA;AAAA,QAAA;AAAA,MAAI;AAAA,IAAA,GAE3G;AAAA,EAER;AAEA,SACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MAAW;AAAA,MAAY;AAAA,MAAsB,OAAO,EAAE,cAAc;AAAA,MAAG,aAAa,EAAE,iBAAiB;AAAA,MAC5F;AAAA,MAAwC;AAAA,MACxC,QAAQ,OAAO,WAAW,aAAa,OAAO,EAAC,QAAQ,iBAAA,CAAiB,IAAI;AAAA,MACnF,UAAA;AAAA,QAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,IAAI,EAAC,IAAI,EAAA,GAAK,UAAA,OAAA,CAAO;AAAA,QAC1D,oBAAC,YAAA,EAAW,UAAU,QAAQ,mBAAA,CAAwC;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAE9E;AAER;"}
@@ -1 +1 @@
1
- {"version":3,"file":"accountConnector.js","sources":["../../src/account/accountConnector.js"],"sourcesContent":["// El conector de la cuenta: lo ÚNICO que un proyecto escribe para tener entrar (con su segundo paso), «¿Olvidó su\n// contraseña?», elegir una contraseña nueva, Mi perfil y su seguridad (@stackcraftmx/calcita/account). Las pantallas solo\n// llaman estas funciones; cada una habla con el backend del proyecto.\n//\n// Tres formas de tenerlo:\n// - createRestAccountConnector(apiClient): si el backend sigue el contrato REST (restAccountConnector.js), ya está;\n// - createDemoAccountConnector() de @stackcraftmx/calcita/account-demo: todo en la memoria del navegador, para arrancar\n// sin backend;\n// - uno propio: un objeto con estas funciones, o el REST con las que no coincidan cambiadas ({...rest, signIn: …}).\n//\n// Cada función regresa una promesa y, si el servidor dice que no, truena con un ApiError (RFC 9457): la pantalla enseña su\n// mensaje, y lo de cada campo en su campo. Las llaves van en camelCase: la traducción desde el backend va en el conector.\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en accountConnector.test.js.\n\n/**\n * Quien está dentro, como lo pintan la barra y Mi perfil.\n *\n * @typedef {object} AccountUser\n * @property {string} id\n * @property {string} name\n * @property {string} email\n * @property {string} [subtitle] el renglón bajo el nombre (puesto · sucursal)\n * @property {string} [phone]\n * @property {string} [avatarUrl]\n */\n\n/**\n * La sesión: quién está dentro y sus permisos (los lee createAccessChecker).\n *\n * @typedef {object} AccountSession\n * @property {AccountUser} user\n * @property {string[]} [roles]\n * @property {string[]} [abilities]\n * @property {string[]} [labels] sus perfiles o etiquetas, ya traducidos (salen como chips en Mi perfil)\n */\n\n/**\n * El estado de la verificación en dos pasos (el que pinta TwoStepVerificationCard).\n *\n * @typedef {object} AccountSecurity\n * @property {boolean} isEnabled\n * @property {string|null} [enabledAt]\n * @property {Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>} passkeys\n * @property {{isEnabled: boolean, enabledAt?: string|null}} totp\n * @property {number} recoveryCodesRemaining\n */\n\n/**\n * Lo que regresa un cambio de seguridad: el estado nuevo y, si se generaron, los códigos de recuperación (se enseñan una\n * sola vez).\n *\n * @typedef {object} AccountSecurityChange\n * @property {AccountSecurity} security\n * @property {string[]} [recoveryCodes]\n */\n\n/**\n * Un equipo donde la cuenta está abierta.\n *\n * @typedef {object} AccountDeviceSession\n * @property {string} id\n * @property {string|null} [browser]\n * @property {string|null} [platform]\n * @property {boolean} [isMobile]\n * @property {string} startedAt instante ISO\n * @property {string|null} [network] a medias (187.190.x.x), nunca la IP completa\n * @property {boolean} [isCurrent]\n */\n\n/**\n * Un renglón de la bitácora de la cuenta.\n *\n * @typedef {object} AccountEvent\n * @property {string} id\n * @property {string} kind uno de ACCOUNT_EVENT_KIND\n * @property {string} occurredAt instante ISO\n * @property {string|null} [device]\n * @property {string|null} [network]\n */\n\n/**\n * El conector completo.\n *\n * @typedef {object} AccountConnector\n * @property {() => Promise<AccountSession|null>} loadSession quién está dentro; null si nadie\n * @property {(credentials: {email: string, password: string, remember: boolean}) => Promise<{secondStep: {methods: string[]}|null}>} signIn\n * con verificación en dos pasos regresa los métodos del segundo paso (SECOND_STEP_METHOD); sin ella, secondStep null\n * y ya está dentro\n * @property {(input: {method: string, code: string}) => Promise<void>} signInSecondStep el código de la app (totp) o uno\n * de recuperación (recoveryCode)\n * @property {() => Promise<object>} secondStepPasskeyOptions las opciones de WebAuthn para firmar (JSON del servidor)\n * @property {(credential: any) => Promise<void>} signInWithPasskey la firma del navegador (JSON de WebAuthn)\n * @property {() => Promise<void>} signOut\n * @property {(input: {email: string}) => Promise<void>} requestPasswordReset contesta igual exista o no el correo\n * @property {(input: {token: string, newPassword: string}) => Promise<void>} resetPassword desde el enlace del correo\n * @property {(input: {name: string, phone: string}) => Promise<{message?: string}|void>} updateProfile\n * @property {(input: {currentPassword?: string, newPassword: string}) => Promise<void>} changePassword\n * @property {() => Promise<AccountSecurity>} loadSecurity\n * @property {() => Promise<{secret: string, qrImageUrl?: string|null}>} startTotp una clave nueva (todavía sin activar)\n * @property {(code: string) => Promise<AccountSecurityChange>} confirmTotp\n * @property {(password: string) => Promise<AccountSecurityChange>} disableTotp\n * @property {() => Promise<object>} passkeyRegistrationOptions las opciones de WebAuthn para crear una llave\n * @property {(input: {name: string, credential: any}) => Promise<AccountSecurityChange>} savePasskey\n * @property {(input: {passkeyId: string, password: string}) => Promise<AccountSecurityChange>} removePasskey\n * @property {(password: string) => Promise<AccountSecurityChange>} regenerateRecoveryCodes\n * @property {() => Promise<AccountDeviceSession[]>} listSessions\n * @property {() => Promise<number>} closeOtherSessions cuántas cerró\n * @property {() => Promise<AccountEvent[]>} listActivity de lo más nuevo a lo más viejo\n * @property {() => Promise<string|null>} [demoTotpCode] SOLO en la demostración: el código que enseñaría la app ahorita\n * (con él sale el botón «Usar el código de ejemplo»)\n */\n\n/** Las funciones que todo conector trae (demoTotpCode es opcional: solo la demostración). */\nexport const ACCOUNT_CONNECTOR_FUNCTIONS = Object.freeze([\n 'loadSession', 'signIn', 'signInSecondStep', 'secondStepPasskeyOptions', 'signInWithPasskey', 'signOut', 'requestPasswordReset',\n 'resetPassword', 'updateProfile', 'changePassword', 'loadSecurity', 'startTotp', 'confirmTotp', 'disableTotp',\n 'passkeyRegistrationOptions', 'savePasskey', 'removePasskey', 'regenerateRecoveryCodes', 'listSessions', 'closeOtherSessions',\n 'listActivity',\n]);\n\n/**\n * Las funciones que le faltan al conector (un error de quien lo escribió: se avisa al arrancar, no al picar un botón).\n *\n * @param {object|null|undefined} connector\n * @returns {string[]}\n */\nexport function missingConnectorFunctionsOf(connector) {\n return ACCOUNT_CONNECTOR_FUNCTIONS.filter((name) => typeof connector?.[name] !== 'function');\n}\n"],"names":[],"mappings":"AAkHY,MAAC,8BAA8B,OAAO,OAAO;AAAA,EACrD;AAAA,EAAe;AAAA,EAAU;AAAA,EAAoB;AAAA,EAA4B;AAAA,EAAqB;AAAA,EAAW;AAAA,EACzG;AAAA,EAAiB;AAAA,EAAiB;AAAA,EAAkB;AAAA,EAAgB;AAAA,EAAa;AAAA,EAAe;AAAA,EAChG;AAAA,EAA8B;AAAA,EAAe;AAAA,EAAiB;AAAA,EAA2B;AAAA,EAAgB;AAAA,EACzG;AACJ,CAAC;AAQM,SAAS,4BAA4B,WAAW;AACnD,SAAO,4BAA4B,OAAO,CAAC,SAAS,OAAO,YAAY,IAAI,MAAM,UAAU;AAC/F;"}
1
+ {"version":3,"file":"accountConnector.js","sources":["../../src/account/accountConnector.js"],"sourcesContent":["// El conector de la cuenta: lo ÚNICO que un proyecto escribe para tener entrar (con su segundo paso), «¿Olvidó su\n// contraseña?», elegir una contraseña nueva, Mi perfil y su seguridad (@stackcraftmx/calcita/account). Las pantallas solo\n// llaman estas funciones; cada una habla con el backend del proyecto.\n//\n// Tres formas de tenerlo:\n// - createRestAccountConnector(apiClient): si el backend sigue el contrato REST (restAccountConnector.js), ya está;\n// - createDemoAccountConnector() de @stackcraftmx/calcita/account-demo: todo en la memoria del navegador, para arrancar\n// sin backend;\n// - uno propio: un objeto con estas funciones, o el REST con las que no coincidan cambiadas ({...rest, signIn: …}).\n//\n// Cada función regresa una promesa y, si el servidor dice que no, truena con un ApiError (RFC 9457): la pantalla enseña su\n// mensaje, y lo de cada campo en su campo. Las llaves van en camelCase: la traducción desde el backend va en el conector.\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en accountConnector.test.js.\n\n/**\n * Quien está dentro, como lo pintan la barra y Mi perfil.\n *\n * @typedef {object} AccountUser\n * @property {string} id\n * @property {string} name\n * @property {string} email\n * @property {string} [subtitle] el renglón bajo el nombre (puesto · sucursal)\n * @property {string} [phone]\n * @property {string} [avatarUrl]\n */\n\n/**\n * La sesión: quién está dentro y sus permisos (los lee createAccessChecker).\n *\n * @typedef {object} AccountSession\n * @property {AccountUser} user\n * @property {string[]} [roles]\n * @property {string[]} [abilities]\n * @property {string[]} [labels] sus perfiles o etiquetas, ya traducidos (salen como chips en Mi perfil)\n */\n\n/**\n * El estado de la verificación en dos pasos (el que pinta TwoStepVerificationCard).\n *\n * @typedef {object} AccountSecurity\n * @property {boolean} isEnabled\n * @property {string|null} [enabledAt]\n * @property {Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>} passkeys\n * @property {{isEnabled: boolean, enabledAt?: string|null}} totp\n * @property {number} recoveryCodesRemaining\n */\n\n/**\n * Lo que regresa un cambio de seguridad: el estado nuevo y, si se generaron, los códigos de recuperación (se enseñan una\n * sola vez).\n *\n * @typedef {object} AccountSecurityChange\n * @property {AccountSecurity} security\n * @property {string[]} [recoveryCodes]\n */\n\n/**\n * Un equipo donde la cuenta está abierta.\n *\n * @typedef {object} AccountDeviceSession\n * @property {string} id\n * @property {string|null} [browser]\n * @property {string|null} [platform]\n * @property {boolean} [isMobile]\n * @property {string} startedAt instante ISO\n * @property {string|null} [network] a medias (187.190.x.x), nunca la IP completa\n * @property {boolean} [isCurrent]\n */\n\n/**\n * Un renglón de la bitácora de la cuenta.\n *\n * @typedef {object} AccountEvent\n * @property {string} id\n * @property {string} kind uno de ACCOUNT_EVENT_KIND\n * @property {string} occurredAt instante ISO\n * @property {string|null} [device]\n * @property {string|null} [network]\n */\n\n/**\n * El conector completo.\n *\n * @typedef {object} AccountConnector\n * @property {() => Promise<AccountSession|null>} loadSession quién está dentro; null si nadie\n * @property {(credentials: {email: string, password: string, remember: boolean}) => Promise<{secondStep: {methods: string[]}|null}>} signIn\n * con verificación en dos pasos regresa los métodos del segundo paso (SECOND_STEP_METHOD); sin ella, secondStep null\n * y ya está dentro\n * @property {(input: {method: string, code: string}) => Promise<void>} signInSecondStep el código de la app (totp) o uno\n * de recuperación (recoveryCode)\n * @property {() => Promise<object>} secondStepPasskeyOptions las opciones de WebAuthn para firmar (JSON del servidor)\n * @property {(credential: any) => Promise<void>} signInWithPasskey la firma del navegador (JSON de WebAuthn)\n * @property {() => Promise<void>} signOut\n * @property {(input: {email: string}) => Promise<void>} requestPasswordReset contesta igual exista o no el correo\n * @property {(input: {token: string, newPassword: string}) => Promise<void>} resetPassword desde el enlace del correo\n * @property {(input: {name: string, phone: string} & Record<string, any>) => Promise<{message?: string}|void>} updateProfile name, phone y\n * los campos propios de ProfileScreen (extraFields)\n * @property {(input: {currentPassword?: string, newPassword: string}) => Promise<void>} changePassword\n * @property {() => Promise<AccountSecurity>} loadSecurity\n * @property {() => Promise<{secret: string, qrImageUrl?: string|null}>} startTotp una clave nueva (todavía sin activar)\n * @property {(code: string) => Promise<AccountSecurityChange>} confirmTotp\n * @property {(password: string) => Promise<AccountSecurityChange>} disableTotp\n * @property {() => Promise<object>} passkeyRegistrationOptions las opciones de WebAuthn para crear una llave\n * @property {(input: {name: string, credential: any}) => Promise<AccountSecurityChange>} savePasskey\n * @property {(input: {passkeyId: string, password: string}) => Promise<AccountSecurityChange>} removePasskey\n * @property {(password: string) => Promise<AccountSecurityChange>} regenerateRecoveryCodes\n * @property {() => Promise<AccountDeviceSession[]>} listSessions\n * @property {() => Promise<number>} closeOtherSessions cuántas cerró\n * @property {() => Promise<AccountEvent[]>} listActivity de lo más nuevo a lo más viejo\n * @property {() => Promise<string|null>} [demoTotpCode] SOLO en la demostración: el código que enseñaría la app ahorita\n * (con él sale el botón «Usar el código de ejemplo»)\n */\n\n/** Las funciones que todo conector trae (demoTotpCode es opcional: solo la demostración). */\nexport const ACCOUNT_CONNECTOR_FUNCTIONS = Object.freeze([\n 'loadSession', 'signIn', 'signInSecondStep', 'secondStepPasskeyOptions', 'signInWithPasskey', 'signOut', 'requestPasswordReset',\n 'resetPassword', 'updateProfile', 'changePassword', 'loadSecurity', 'startTotp', 'confirmTotp', 'disableTotp',\n 'passkeyRegistrationOptions', 'savePasskey', 'removePasskey', 'regenerateRecoveryCodes', 'listSessions', 'closeOtherSessions',\n 'listActivity',\n]);\n\n/**\n * Las funciones que le faltan al conector (un error de quien lo escribió: se avisa al arrancar, no al picar un botón).\n *\n * @param {object|null|undefined} connector\n * @returns {string[]}\n */\nexport function missingConnectorFunctionsOf(connector) {\n return ACCOUNT_CONNECTOR_FUNCTIONS.filter((name) => typeof connector?.[name] !== 'function');\n}\n"],"names":[],"mappings":"AAmHY,MAAC,8BAA8B,OAAO,OAAO;AAAA,EACrD;AAAA,EAAe;AAAA,EAAU;AAAA,EAAoB;AAAA,EAA4B;AAAA,EAAqB;AAAA,EAAW;AAAA,EACzG;AAAA,EAAiB;AAAA,EAAiB;AAAA,EAAkB;AAAA,EAAgB;AAAA,EAAa;AAAA,EAAe;AAAA,EAChG;AAAA,EAA8B;AAAA,EAAe;AAAA,EAAiB;AAAA,EAA2B;AAAA,EAAgB;AAAA,EACzG;AACJ,CAAC;AAQM,SAAS,4BAA4B,WAAW;AACnD,SAAO,4BAA4B,OAAO,CAAC,SAAS,OAAO,YAAY,IAAI,MAAM,UAAU;AAC/F;"}
@@ -72,7 +72,8 @@ function createRestAccountConnector(apiClient, { paths = {} } = {}) {
72
72
  resetPassword: async ({ token, newPassword }) => {
73
73
  await apiClient.post(path.resetPassword, { token, new_password: newPassword });
74
74
  },
75
- updateProfile: async ({ name, phone }) => await apiClient.put(path.profile, { name, phone }) ?? {},
75
+ // todo lo del formulario viaja: name, phone y los campos propios de ProfileScreen (extraFields) con sus nombres
76
+ updateProfile: async (profile) => await apiClient.put(path.profile, profile) ?? {},
76
77
  changePassword: async ({ currentPassword, newPassword }) => {
77
78
  await apiClient.put(path.password, { current_password: currentPassword, new_password: newPassword });
78
79
  },
@@ -1 +1 @@
1
- {"version":3,"file":"restAccountConnector.js","sources":["../../src/account/restAccountConnector.js"],"sourcesContent":["// El conector de la cuenta para un backend REST que sigue este contrato (llaves en snake_case, errores RFC 9457). Si el\n// backend lo sigue, conectar la cuenta es una línea:\n//\n// export const accountConnector = createRestAccountConnector(apiClient);\n//\n// Si alguna ruta se llama distinto, se cambia en `paths`; si alguna respuesta tiene otra forma, se reemplaza esa función:\n//\n// export const accountConnector = {...createRestAccountConnector(apiClient), signIn: async (credentials) => …};\n//\n// El contrato (las rutas por omisión, ACCOUNT_REST_PATHS):\n//\n// GET /auth/session → {user, roles, abilities, labels} | 401 si nadie está dentro\n// POST /auth/sign-in {email, password, remember} → {second_step: {methods} | null}\n// POST /auth/second-step {method, code} → 204 (method: totp | recovery_code)\n// GET /auth/second-step/passkey-options → opciones de WebAuthn para firmar\n// POST /auth/second-step/passkey {credential} → 204\n// POST /auth/sign-out → 204\n// POST /auth/forgot-password {email} → 204 (igual exista o no el correo)\n// POST /auth/reset-password {token, new_password} → 204\n// PUT /account {name, phone} → {message?}\n// PUT /account/password {current_password, new_password} → 204\n// GET /account/security → el estado de la verificación (abajo)\n// POST /account/security/totp → {secret, qr_image_url}\n// POST /account/security/totp/confirm {code} → {security, recovery_codes?}\n// DELETE /account/security/totp {password} → {security}\n// GET /account/security/passkeys/registration-options → opciones de WebAuthn para crear\n// POST /account/security/passkeys {name, credential} → {security, recovery_codes?}\n// DELETE /account/security/passkeys/{id} {password} → {security}\n// POST /account/security/recovery-codes {password} → {security, recovery_codes}\n// GET /account/sessions → [{id, browser, platform, is_mobile, started_at, network, is_current}]\n// POST /account/sessions/close-others → {closed}\n// GET /account/activity → [{id, kind, occurred_at, device, network}]\n//\n// El estado de la verificación: {is_enabled, enabled_at, passkeys: [{id, name, created_at, last_used_at}],\n// totp: {is_enabled, enabled_at}, recovery_codes_remaining}. Las opciones y la firma de WebAuthn viajan tal cual (las arma\n// y las revisa la librería del servidor: py_webauthn, @simplewebauthn/server, web-auth/webauthn-lib).\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en restAccountConnector.test.js.\n\n/** Las rutas del contrato; se pegan a la baseUrl del cliente (/api + /auth/session). */\nexport const ACCOUNT_REST_PATHS = Object.freeze({\n session: '/auth/session',\n signIn: '/auth/sign-in',\n secondStep: '/auth/second-step',\n secondStepPasskeyOptions: '/auth/second-step/passkey-options',\n secondStepPasskey: '/auth/second-step/passkey',\n signOut: '/auth/sign-out',\n forgotPassword: '/auth/forgot-password',\n resetPassword: '/auth/reset-password',\n profile: '/account',\n password: '/account/password',\n security: '/account/security',\n totp: '/account/security/totp',\n totpConfirm: '/account/security/totp/confirm',\n passkeyRegistrationOptions: '/account/security/passkeys/registration-options',\n passkeys: '/account/security/passkeys',\n recoveryCodes: '/account/security/recovery-codes',\n sessions: '/account/sessions',\n closeOtherSessions: '/account/sessions/close-others',\n activity: '/account/activity',\n});\n\n// cómo se llama cada método del segundo paso en el contrato (snake_case) y en las pantallas (SECOND_STEP_METHOD)\nconst SECOND_STEP_METHOD_IN_API = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recovery_code'});\nconst SECOND_STEP_METHOD_FROM_API = Object.freeze({passkey: 'passkey', totp: 'totp', recovery_code: 'recoveryCode'});\n\n/**\n * El estado de la verificación, del contrato a camelCase.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurity}\n */\nexport function securityFromResponse(response) {\n return {\n isEnabled: Boolean(response.is_enabled),\n enabledAt: response.enabled_at ?? null,\n passkeys: (response.passkeys ?? []).map((passkey) => ({\n id: passkey.id, name: passkey.name, createdAt: passkey.created_at, lastUsedAt: passkey.last_used_at ?? null,\n })),\n totp: {isEnabled: Boolean(response.totp?.is_enabled), enabledAt: response.totp?.enabled_at ?? null},\n recoveryCodesRemaining: response.recovery_codes_remaining ?? 0,\n };\n}\n\n/**\n * Lo que regresa un cambio de seguridad: {security, recovery_codes?} → {security, recoveryCodes?}.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurityChange}\n */\nexport function securityChangeFromResponse(response) {\n return {security: securityFromResponse(response.security), recoveryCodes: response.recovery_codes ?? undefined};\n}\n\n/**\n * @param {{get: Function, post: Function, put: Function, delete: Function}} apiClient el de createApiClient\n * @param {{paths?: Partial<Record<keyof typeof ACCOUNT_REST_PATHS, string>>}} [options] las rutas que se llaman distinto\n * @returns {import('@/account/accountConnector.js').AccountConnector}\n */\nexport function createRestAccountConnector(apiClient, {paths = {}} = {}) {\n const path = {...ACCOUNT_REST_PATHS, ...paths};\n\n return {\n loadSession: async () => {\n try {\n // sin el aviso de «sesión vencida»: aquí que no haya sesión es una respuesta normal\n return await apiClient.get(path.session, {skipUnauthenticatedHandler: true});\n } catch (error) {\n if (error?.isUnauthenticated) {\n return null;\n }\n throw error;\n }\n },\n signIn: async ({email, password, remember}) => {\n const response = await apiClient.post(path.signIn, {email, password, remember}, {skipUnauthenticatedHandler: true});\n const methods = response?.second_step?.methods;\n return {secondStep: methods ? {methods: methods.map((method) => SECOND_STEP_METHOD_FROM_API[method] ?? method)} : null};\n },\n signInSecondStep: async ({method, code}) => {\n await apiClient.post(path.secondStep, {method: SECOND_STEP_METHOD_IN_API[method] ?? method, code}, {skipUnauthenticatedHandler: true});\n },\n secondStepPasskeyOptions: () => apiClient.get(path.secondStepPasskeyOptions, {skipUnauthenticatedHandler: true}),\n signInWithPasskey: async (credential) => {\n await apiClient.post(path.secondStepPasskey, {credential}, {skipUnauthenticatedHandler: true});\n },\n signOut: async () => {\n await apiClient.post(path.signOut, undefined, {skipUnauthenticatedHandler: true});\n },\n requestPasswordReset: async ({email}) => {\n await apiClient.post(path.forgotPassword, {email});\n },\n resetPassword: async ({token, newPassword}) => {\n await apiClient.post(path.resetPassword, {token, new_password: newPassword});\n },\n updateProfile: async ({name, phone}) => (await apiClient.put(path.profile, {name, phone})) ?? {},\n changePassword: async ({currentPassword, newPassword}) => {\n await apiClient.put(path.password, {current_password: currentPassword, new_password: newPassword});\n },\n loadSecurity: async () => securityFromResponse(await apiClient.get(path.security)),\n startTotp: async () => {\n const response = await apiClient.post(path.totp);\n return {secret: response.secret, qrImageUrl: response.qr_image_url ?? null};\n },\n confirmTotp: async (code) => securityChangeFromResponse(await apiClient.post(path.totpConfirm, {code})),\n disableTotp: async (password) => securityChangeFromResponse(await apiClient.delete(path.totp, {body: {password}})),\n passkeyRegistrationOptions: () => apiClient.get(path.passkeyRegistrationOptions),\n savePasskey: async ({name, credential}) => securityChangeFromResponse(await apiClient.post(path.passkeys, {name, credential})),\n removePasskey: async ({passkeyId, password}) => securityChangeFromResponse(\n await apiClient.delete(`${path.passkeys}/${encodeURIComponent(passkeyId)}`, {body: {password}}),\n ),\n regenerateRecoveryCodes: async (password) => securityChangeFromResponse(await apiClient.post(path.recoveryCodes, {password})),\n listSessions: async () => (await apiClient.get(path.sessions)).map((session) => ({\n id: session.id, browser: session.browser, platform: session.platform, isMobile: Boolean(session.is_mobile),\n startedAt: session.started_at, network: session.network, isCurrent: Boolean(session.is_current),\n })),\n closeOtherSessions: async () => (await apiClient.post(path.closeOtherSessions))?.closed ?? 0,\n listActivity: async () => (await apiClient.get(path.activity)).map((event) => ({\n id: event.id, kind: event.kind, occurredAt: event.occurred_at, device: event.device, network: event.network,\n })),\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"restAccountConnector.js","sources":["../../src/account/restAccountConnector.js"],"sourcesContent":["// El conector de la cuenta para un backend REST que sigue este contrato (llaves en snake_case, errores RFC 9457). Si el\n// backend lo sigue, conectar la cuenta es una línea:\n//\n// export const accountConnector = createRestAccountConnector(apiClient);\n//\n// Si alguna ruta se llama distinto, se cambia en `paths`; si alguna respuesta tiene otra forma, se reemplaza esa función:\n//\n// export const accountConnector = {...createRestAccountConnector(apiClient), signIn: async (credentials) => …};\n//\n// El contrato (las rutas por omisión, ACCOUNT_REST_PATHS):\n//\n// GET /auth/session → {user, roles, abilities, labels} | 401 si nadie está dentro\n// POST /auth/sign-in {email, password, remember} → {second_step: {methods} | null}\n// POST /auth/second-step {method, code} → 204 (method: totp | recovery_code)\n// GET /auth/second-step/passkey-options → opciones de WebAuthn para firmar\n// POST /auth/second-step/passkey {credential} → 204\n// POST /auth/sign-out → 204\n// POST /auth/forgot-password {email} → 204 (igual exista o no el correo)\n// POST /auth/reset-password {token, new_password} → 204\n// PUT /account {name, phone} → {message?}\n// PUT /account/password {current_password, new_password} → 204\n// GET /account/security → el estado de la verificación (abajo)\n// POST /account/security/totp → {secret, qr_image_url}\n// POST /account/security/totp/confirm {code} → {security, recovery_codes?}\n// DELETE /account/security/totp {password} → {security}\n// GET /account/security/passkeys/registration-options → opciones de WebAuthn para crear\n// POST /account/security/passkeys {name, credential} → {security, recovery_codes?}\n// DELETE /account/security/passkeys/{id} {password} → {security}\n// POST /account/security/recovery-codes {password} → {security, recovery_codes}\n// GET /account/sessions → [{id, browser, platform, is_mobile, started_at, network, is_current}]\n// POST /account/sessions/close-others → {closed}\n// GET /account/activity → [{id, kind, occurred_at, device, network}]\n//\n// El estado de la verificación: {is_enabled, enabled_at, passkeys: [{id, name, created_at, last_used_at}],\n// totp: {is_enabled, enabled_at}, recovery_codes_remaining}. Las opciones y la firma de WebAuthn viajan tal cual (las arma\n// y las revisa la librería del servidor: py_webauthn, @simplewebauthn/server, web-auth/webauthn-lib).\n//\n// Funciones puras, no renderizan: function (regla de la casa). Pruebas en restAccountConnector.test.js.\n\n/** Las rutas del contrato; se pegan a la baseUrl del cliente (/api + /auth/session). */\nexport const ACCOUNT_REST_PATHS = Object.freeze({\n session: '/auth/session',\n signIn: '/auth/sign-in',\n secondStep: '/auth/second-step',\n secondStepPasskeyOptions: '/auth/second-step/passkey-options',\n secondStepPasskey: '/auth/second-step/passkey',\n signOut: '/auth/sign-out',\n forgotPassword: '/auth/forgot-password',\n resetPassword: '/auth/reset-password',\n profile: '/account',\n password: '/account/password',\n security: '/account/security',\n totp: '/account/security/totp',\n totpConfirm: '/account/security/totp/confirm',\n passkeyRegistrationOptions: '/account/security/passkeys/registration-options',\n passkeys: '/account/security/passkeys',\n recoveryCodes: '/account/security/recovery-codes',\n sessions: '/account/sessions',\n closeOtherSessions: '/account/sessions/close-others',\n activity: '/account/activity',\n});\n\n// cómo se llama cada método del segundo paso en el contrato (snake_case) y en las pantallas (SECOND_STEP_METHOD)\nconst SECOND_STEP_METHOD_IN_API = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recovery_code'});\nconst SECOND_STEP_METHOD_FROM_API = Object.freeze({passkey: 'passkey', totp: 'totp', recovery_code: 'recoveryCode'});\n\n/**\n * El estado de la verificación, del contrato a camelCase.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurity}\n */\nexport function securityFromResponse(response) {\n return {\n isEnabled: Boolean(response.is_enabled),\n enabledAt: response.enabled_at ?? null,\n passkeys: (response.passkeys ?? []).map((passkey) => ({\n id: passkey.id, name: passkey.name, createdAt: passkey.created_at, lastUsedAt: passkey.last_used_at ?? null,\n })),\n totp: {isEnabled: Boolean(response.totp?.is_enabled), enabledAt: response.totp?.enabled_at ?? null},\n recoveryCodesRemaining: response.recovery_codes_remaining ?? 0,\n };\n}\n\n/**\n * Lo que regresa un cambio de seguridad: {security, recovery_codes?} → {security, recoveryCodes?}.\n *\n * @param {object} response\n * @returns {import('@/account/accountConnector.js').AccountSecurityChange}\n */\nexport function securityChangeFromResponse(response) {\n return {security: securityFromResponse(response.security), recoveryCodes: response.recovery_codes ?? undefined};\n}\n\n/**\n * @param {{get: Function, post: Function, put: Function, delete: Function}} apiClient el de createApiClient\n * @param {{paths?: Partial<Record<keyof typeof ACCOUNT_REST_PATHS, string>>}} [options] las rutas que se llaman distinto\n * @returns {import('@/account/accountConnector.js').AccountConnector}\n */\nexport function createRestAccountConnector(apiClient, {paths = {}} = {}) {\n const path = {...ACCOUNT_REST_PATHS, ...paths};\n\n return {\n loadSession: async () => {\n try {\n // sin el aviso de «sesión vencida»: aquí que no haya sesión es una respuesta normal\n return await apiClient.get(path.session, {skipUnauthenticatedHandler: true});\n } catch (error) {\n if (error?.isUnauthenticated) {\n return null;\n }\n throw error;\n }\n },\n signIn: async ({email, password, remember}) => {\n const response = await apiClient.post(path.signIn, {email, password, remember}, {skipUnauthenticatedHandler: true});\n const methods = response?.second_step?.methods;\n return {secondStep: methods ? {methods: methods.map((method) => SECOND_STEP_METHOD_FROM_API[method] ?? method)} : null};\n },\n signInSecondStep: async ({method, code}) => {\n await apiClient.post(path.secondStep, {method: SECOND_STEP_METHOD_IN_API[method] ?? method, code}, {skipUnauthenticatedHandler: true});\n },\n secondStepPasskeyOptions: () => apiClient.get(path.secondStepPasskeyOptions, {skipUnauthenticatedHandler: true}),\n signInWithPasskey: async (credential) => {\n await apiClient.post(path.secondStepPasskey, {credential}, {skipUnauthenticatedHandler: true});\n },\n signOut: async () => {\n await apiClient.post(path.signOut, undefined, {skipUnauthenticatedHandler: true});\n },\n requestPasswordReset: async ({email}) => {\n await apiClient.post(path.forgotPassword, {email});\n },\n resetPassword: async ({token, newPassword}) => {\n await apiClient.post(path.resetPassword, {token, new_password: newPassword});\n },\n // todo lo del formulario viaja: name, phone y los campos propios de ProfileScreen (extraFields) con sus nombres\n updateProfile: async (profile) => (await apiClient.put(path.profile, profile)) ?? {},\n changePassword: async ({currentPassword, newPassword}) => {\n await apiClient.put(path.password, {current_password: currentPassword, new_password: newPassword});\n },\n loadSecurity: async () => securityFromResponse(await apiClient.get(path.security)),\n startTotp: async () => {\n const response = await apiClient.post(path.totp);\n return {secret: response.secret, qrImageUrl: response.qr_image_url ?? null};\n },\n confirmTotp: async (code) => securityChangeFromResponse(await apiClient.post(path.totpConfirm, {code})),\n disableTotp: async (password) => securityChangeFromResponse(await apiClient.delete(path.totp, {body: {password}})),\n passkeyRegistrationOptions: () => apiClient.get(path.passkeyRegistrationOptions),\n savePasskey: async ({name, credential}) => securityChangeFromResponse(await apiClient.post(path.passkeys, {name, credential})),\n removePasskey: async ({passkeyId, password}) => securityChangeFromResponse(\n await apiClient.delete(`${path.passkeys}/${encodeURIComponent(passkeyId)}`, {body: {password}}),\n ),\n regenerateRecoveryCodes: async (password) => securityChangeFromResponse(await apiClient.post(path.recoveryCodes, {password})),\n listSessions: async () => (await apiClient.get(path.sessions)).map((session) => ({\n id: session.id, browser: session.browser, platform: session.platform, isMobile: Boolean(session.is_mobile),\n startedAt: session.started_at, network: session.network, isCurrent: Boolean(session.is_current),\n })),\n closeOtherSessions: async () => (await apiClient.post(path.closeOtherSessions))?.closed ?? 0,\n listActivity: async () => (await apiClient.get(path.activity)).map((event) => ({\n id: event.id, kind: event.kind, occurredAt: event.occurred_at, device: event.device, network: event.network,\n })),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -8,12 +8,12 @@ import { createLogger } from "../observability/logger.js";
8
8
  import ErrorPage, { ERROR_PAGE_KIND } from "../ui/layout/ErrorPage.js";
9
9
  const log = createLogger("navigation");
10
10
  const NOT_FOUND_STATUS = 404;
11
- function AccessOutlet({ access }) {
11
+ function AccessOutlet({ access, fallback }) {
12
12
  const parentContext = useOutletContext();
13
- return /* @__PURE__ */ jsx(RequireAccess, { access, children: /* @__PURE__ */ jsx(Outlet, { context: parentContext }) });
13
+ return /* @__PURE__ */ jsx(RequireAccess, { access, fallback, children: /* @__PURE__ */ jsx(Outlet, { context: parentContext }) });
14
14
  }
15
- function withAccess(access, routes) {
16
- return { element: /* @__PURE__ */ jsx(AccessOutlet, { access }), children: routes };
15
+ function withAccess(access, routes, { fallback } = {}) {
16
+ return { element: /* @__PURE__ */ jsx(AccessOutlet, { access, fallback }), children: routes };
17
17
  }
18
18
  function useIsNavigating() {
19
19
  return useNavigation().state === "loading";
@@ -28,7 +28,7 @@ function withChunkRecovery(routes) {
28
28
  function reloadPage() {
29
29
  window.location.reload();
30
30
  }
31
- function RouteErrorScreen() {
31
+ function RouteErrorScreen({ onError, ...errorPageProps } = {}) {
32
32
  const error = useRouteError();
33
33
  const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;
34
34
  const kind = isNotFound ? ERROR_PAGE_KIND.notFound : isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError;
@@ -36,8 +36,9 @@ function RouteErrorScreen() {
36
36
  if (!isNotFound) {
37
37
  log.error("una pantalla no se pudo pintar", { name: error?.name, message: error?.message ?? String(error) });
38
38
  }
39
+ onError?.(error, kind);
39
40
  }, [error, isNotFound]);
40
- return /* @__PURE__ */ jsx(ErrorPage, { kind, onRetry: isNotFound ? void 0 : reloadPage });
41
+ return /* @__PURE__ */ jsx(ErrorPage, { kind, onRetry: isNotFound ? void 0 : reloadPage, ...errorPageProps });
41
42
  }
42
43
  export {
43
44
  RouteErrorScreen,
@@ -1 +1 @@
1
- {"version":3,"file":"reactRouterRoutes.js","sources":["../../src/adapters/reactRouterRoutes.jsx"],"sourcesContent":["// Lo que conecta los permisos y la barra con react-router (@stackcraftmx/calcita/react-router):\n//\n// withAccess(access, routes) un grupo de rutas con la regla del menu.js de su módulo: quien no puede ve la página de\n// «sin permiso» en lugar de la pantalla\n// useIsNavigating() la pantalla siguiente se está descargando (rutas lazy): para la rayita de AppLayout\n// withChunkRecovery(routes) las rutas lazy recargan UNA vez si su archivo es de una versión que ya no existe\n// RouteErrorScreen la pantalla de error de una ruta (errorElement / ErrorBoundary del router): «Hay una\n// versión nueva» o «Algo falló», con su botón para recargar\n//\n// import {access} from '@/modules/users/menu.js';\n// import {routes} from '@/modules/users/routes.js';\n// children: [withAccess(access, routes)]\n//\n// OJO: useIsNavigating solo funciona con un router de datos (createBrowserRouter); con <BrowserRouter> no hay estado de\n// navegación.\n//\n// Genéricos del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {Outlet, isRouteErrorResponse, useNavigation, useOutletContext, useRouteError} from 'react-router';\n\nimport RequireAccess from '@/access/RequireAccess.jsx';\nimport {isStaleChunkError, recoverFromStaleChunk} from '@/api/staleChunks.js';\nimport {createLogger} from '@/observability/logger.js';\nimport ErrorPage, {ERROR_PAGE_KIND} from '@/ui/layout/ErrorPage.jsx';\n\nconst log = createLogger('navigation');\nconst NOT_FOUND_STATUS = 404;\n\n/**\n * El grupo con permiso: RequireAccess alrededor de las rutas hijas.\n *\n * OJO, trampa pagada: un <Outlet /> nuevo corta el contexto que la ruta de arriba le pasa a sus hijas (<Outlet\n * context={...} />, useOutletContext); el inicio de los ejemplos tronaba sin el menú. Aquí se pasa tal cual.\n *\n * @param {{access: object|undefined}} props\n */\nfunction AccessOutlet({access}) {\n const parentContext = useOutletContext();\n return <RequireAccess access={access}><Outlet context={parentContext} /></RequireAccess>;\n}\n\n/**\n * Un grupo de rutas sin camino propio que pide permiso antes de pintar a sus hijas.\n *\n * @param {object|undefined} access la regla (accessRules.js); sin ella, el grupo es de todos\n * @param {object[]} routes las rutas del módulo\n * @returns {object} la ruta de react-router\n */\nexport function withAccess(access, routes) {\n return {element: <AccessOutlet access={access} />, children: routes};\n}\n\n/**\n * ¿Se está descargando la pantalla siguiente?\n *\n * @returns {boolean}\n */\nexport function useIsNavigating() {\n return useNavigation().state === 'loading';\n}\n\n/**\n * Las rutas con la recuperación de archivos viejos (staleChunks.js): cada `lazy`, también la de las hijas, recarga la\n * página UNA vez si el archivo de su pantalla ya no existe porque se publicó otra versión. Lo demás de la ruta no cambia.\n *\n * @param {object[]} routes\n * @returns {object[]}\n */\nexport function withChunkRecovery(routes) {\n return routes.map((route) => ({\n ...route,\n ...(typeof route.lazy === 'function' && {lazy: () => route.lazy().catch(recoverFromStaleChunk)}),\n ...(route.children && {children: withChunkRecovery(route.children)}),\n }));\n}\n\n/** Recargar la página: trae la versión publicada. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La pantalla de error de una ruta (la que el router enseña cuando la pantalla truena o no se pudo descargar): una ruta\n * que no existe es «Esta página no existe»; un archivo de otra versión, «Hay una versión nueva»; lo demás, «Algo falló de\n * nuestro lado». Las dos últimas con su botón para recargar.\n *\n * createBrowserRouter([{path: '/', element: <App />, ErrorBoundary: RouteErrorScreen, children: [...]}])\n */\nexport function RouteErrorScreen() {\n const error = useRouteError();\n const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;\n const kind = isNotFound ? ERROR_PAGE_KIND.notFound : (isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError);\n\n useEffect(() => {\n if (!isNotFound) {\n log.error('una pantalla no se pudo pintar', {name: error?.name, message: error?.message ?? String(error)});\n }\n }, [error, isNotFound]);\n\n return <ErrorPage kind={kind} onRetry={isNotFound ? undefined : reloadPage} />;\n}\n"],"names":[],"mappings":";;;;;;;;AAyBA,MAAM,MAAM,aAAa,YAAY;AACrC,MAAM,mBAAmB;AAUzB,SAAS,aAAa,EAAC,UAAS;AAC5B,QAAM,gBAAgB,iBAAA;AACtB,6BAAQ,eAAA,EAAc,QAAgB,8BAAC,QAAA,EAAO,SAAS,eAAe,EAAA,CAAE;AAC5E;AASO,SAAS,WAAW,QAAQ,QAAQ;AACvC,SAAO,EAAC,SAAS,oBAAC,gBAAa,QAAgB,GAAI,UAAU,OAAA;AACjE;AAOO,SAAS,kBAAkB;AAC9B,SAAO,cAAA,EAAgB,UAAU;AACrC;AASO,SAAS,kBAAkB,QAAQ;AACtC,SAAO,OAAO,IAAI,CAAC,WAAW;AAAA,IAC1B,GAAG;AAAA,IACH,GAAI,OAAO,MAAM,SAAS,cAAc,EAAC,MAAM,MAAM,MAAM,KAAA,EAAO,MAAM,qBAAqB,EAAA;AAAA,IAC7F,GAAI,MAAM,YAAY,EAAC,UAAU,kBAAkB,MAAM,QAAQ,EAAA;AAAA,EAAC,EACpE;AACN;AAGA,SAAS,aAAa;AAClB,SAAO,SAAS,OAAA;AACpB;AASO,SAAS,mBAAmB;AAC/B,QAAM,QAAQ,cAAA;AACd,QAAM,aAAa,qBAAqB,KAAK,KAAK,MAAM,WAAW;AACnE,QAAM,OAAO,aAAa,gBAAgB,WAAY,kBAAkB,KAAK,IAAI,gBAAgB,aAAa,gBAAgB;AAE9H,YAAU,MAAM;AACZ,QAAI,CAAC,YAAY;AACb,UAAI,MAAM,kCAAkC,EAAC,MAAM,OAAO,MAAM,SAAS,OAAO,WAAW,OAAO,KAAK,EAAA,CAAE;AAAA,IAC7G;AAAA,EACJ,GAAG,CAAC,OAAO,UAAU,CAAC;AAEtB,6BAAQ,WAAA,EAAU,MAAY,SAAS,aAAa,SAAY,YAAY;AAChF;"}
1
+ {"version":3,"file":"reactRouterRoutes.js","sources":["../../src/adapters/reactRouterRoutes.jsx"],"sourcesContent":["// Lo que conecta los permisos y la barra con react-router (@stackcraftmx/calcita/react-router):\n//\n// withAccess(access, routes) un grupo de rutas con la regla del menu.js de su módulo: quien no puede ve la página de\n// «sin permiso» en lugar de la pantalla\n// useIsNavigating() la pantalla siguiente se está descargando (rutas lazy): para la rayita de AppLayout\n// withChunkRecovery(routes) las rutas lazy recargan UNA vez si su archivo es de una versión que ya no existe\n// RouteErrorScreen la pantalla de error de una ruta (errorElement / ErrorBoundary del router): «Hay una\n// versión nueva» o «Algo falló», con su botón para recargar\n//\n// import {access} from '@/modules/users/menu.js';\n// import {routes} from '@/modules/users/routes.js';\n// children: [withAccess(access, routes)]\n//\n// OJO: useIsNavigating solo funciona con un router de datos (createBrowserRouter); con <BrowserRouter> no hay estado de\n// navegación.\n//\n// Genéricos del paquete: function (regla de la casa).\nimport {useEffect} from 'react';\nimport {Outlet, isRouteErrorResponse, useNavigation, useOutletContext, useRouteError} from 'react-router';\n\nimport RequireAccess from '@/access/RequireAccess.jsx';\nimport {isStaleChunkError, recoverFromStaleChunk} from '@/api/staleChunks.js';\nimport {createLogger} from '@/observability/logger.js';\nimport ErrorPage, {ERROR_PAGE_KIND} from '@/ui/layout/ErrorPage.jsx';\n\nconst log = createLogger('navigation');\nconst NOT_FOUND_STATUS = 404;\n\n/**\n * El grupo con permiso: RequireAccess alrededor de las rutas hijas.\n *\n * OJO, trampa pagada: un <Outlet /> nuevo corta el contexto que la ruta de arriba le pasa a sus hijas (<Outlet\n * context={...} />, useOutletContext); el inicio de los ejemplos tronaba sin el menú. Aquí se pasa tal cual.\n *\n * @param {{access: object|undefined, fallback?: import('react').ReactNode}} props\n */\nfunction AccessOutlet({access, fallback}) {\n const parentContext = useOutletContext();\n return <RequireAccess access={access} fallback={fallback}><Outlet context={parentContext} /></RequireAccess>;\n}\n\n/**\n * Un grupo de rutas sin camino propio que pide permiso antes de pintar a sus hijas.\n *\n * @param {object|undefined} access la regla (accessRules.js); sin ella, el grupo es de todos\n * @param {object[]} routes las rutas del módulo\n * @param {{fallback?: import('react').ReactNode}} [options] fallback: lo que ve quien no tiene permiso; por omisión la página\n * de «sin permiso» de la casa (con ErrorPage y sus props se arma una propia: un botón para pedir acceso)\n * @returns {object} la ruta de react-router\n */\nexport function withAccess(access, routes, {fallback} = {}) {\n return {element: <AccessOutlet access={access} fallback={fallback} />, children: routes};\n}\n\n/**\n * ¿Se está descargando la pantalla siguiente?\n *\n * @returns {boolean}\n */\nexport function useIsNavigating() {\n return useNavigation().state === 'loading';\n}\n\n/**\n * Las rutas con la recuperación de archivos viejos (staleChunks.js): cada `lazy`, también la de las hijas, recarga la\n * página UNA vez si el archivo de su pantalla ya no existe porque se publicó otra versión. Lo demás de la ruta no cambia.\n *\n * @param {object[]} routes\n * @returns {object[]}\n */\nexport function withChunkRecovery(routes) {\n return routes.map((route) => ({\n ...route,\n ...(typeof route.lazy === 'function' && {lazy: () => route.lazy().catch(recoverFromStaleChunk)}),\n ...(route.children && {children: withChunkRecovery(route.children)}),\n }));\n}\n\n/** Recargar la página: trae la versión publicada. */\nfunction reloadPage() {\n window.location.reload();\n}\n\n/**\n * La pantalla de error de una ruta (la que el router enseña cuando la pantalla truena o no se pudo descargar): una ruta\n * que no existe es «Esta página no existe»; un archivo de otra versión, «Hay una versión nueva»; lo demás, «Algo falló de\n * nuestro lado». Las dos últimas con su botón para recargar.\n *\n * createBrowserRouter([{path: '/', element: <App />, ErrorBoundary: RouteErrorScreen, children: [...]}])\n *\n * Con props (como errorElement) se extiende igual que ErrorPage, y onError recibe lo que tronó (para Sentry o su aviso):\n * {errorElement: <RouteErrorScreen onError={(error) => reportError(error)} extraActions={<SupportButton />} />}\n *\n * @param {object} [props] las de ErrorPage (extraActions, actions, children, icon, homePath, onGoHome, texts) y:\n * @param {(error: unknown, kind: string) => void} [props.onError] cada error que llega aquí, con su tipo (ERROR_PAGE_KIND)\n */\nexport function RouteErrorScreen({onError, ...errorPageProps} = {}) {\n const error = useRouteError();\n const isNotFound = isRouteErrorResponse(error) && error.status === NOT_FOUND_STATUS;\n const kind = isNotFound ? ERROR_PAGE_KIND.notFound : (isStaleChunkError(error) ? ERROR_PAGE_KIND.newVersion : ERROR_PAGE_KIND.serverError);\n\n useEffect(() => {\n if (!isNotFound) {\n log.error('una pantalla no se pudo pintar', {name: error?.name, message: error?.message ?? String(error)});\n }\n onError?.(error, kind);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [error, isNotFound]);\n\n return <ErrorPage kind={kind} onRetry={isNotFound ? undefined : reloadPage} {...errorPageProps} />;\n}\n"],"names":[],"mappings":";;;;;;;;AAyBA,MAAM,MAAM,aAAa,YAAY;AACrC,MAAM,mBAAmB;AAUzB,SAAS,aAAa,EAAC,QAAQ,YAAW;AACtC,QAAM,gBAAgB,iBAAA;AACtB,SAAO,oBAAC,iBAAc,QAAgB,UAAoB,8BAAC,QAAA,EAAO,SAAS,eAAe,EAAA,CAAE;AAChG;AAWO,SAAS,WAAW,QAAQ,QAAQ,EAAC,SAAA,IAAY,CAAA,GAAI;AACxD,SAAO,EAAC,SAAS,oBAAC,cAAA,EAAa,QAAgB,SAAA,CAAoB,GAAI,UAAU,OAAA;AACrF;AAOO,SAAS,kBAAkB;AAC9B,SAAO,cAAA,EAAgB,UAAU;AACrC;AASO,SAAS,kBAAkB,QAAQ;AACtC,SAAO,OAAO,IAAI,CAAC,WAAW;AAAA,IAC1B,GAAG;AAAA,IACH,GAAI,OAAO,MAAM,SAAS,cAAc,EAAC,MAAM,MAAM,MAAM,KAAA,EAAO,MAAM,qBAAqB,EAAA;AAAA,IAC7F,GAAI,MAAM,YAAY,EAAC,UAAU,kBAAkB,MAAM,QAAQ,EAAA;AAAA,EAAC,EACpE;AACN;AAGA,SAAS,aAAa;AAClB,SAAO,SAAS,OAAA;AACpB;AAeO,SAAS,iBAAiB,EAAC,SAAS,GAAG,eAAA,IAAkB,CAAA,GAAI;AAChE,QAAM,QAAQ,cAAA;AACd,QAAM,aAAa,qBAAqB,KAAK,KAAK,MAAM,WAAW;AACnE,QAAM,OAAO,aAAa,gBAAgB,WAAY,kBAAkB,KAAK,IAAI,gBAAgB,aAAa,gBAAgB;AAE9H,YAAU,MAAM;AACZ,QAAI,CAAC,YAAY;AACb,UAAI,MAAM,kCAAkC,EAAC,MAAM,OAAO,MAAM,SAAS,OAAO,WAAW,OAAO,KAAK,EAAA,CAAE;AAAA,IAC7G;AACA,cAAU,OAAO,IAAI;AAAA,EAEzB,GAAG,CAAC,OAAO,UAAU,CAAC;AAEtB,SAAO,oBAAC,aAAU,MAAY,SAAS,aAAa,SAAY,YAAa,GAAG,gBAAgB;AACpG;"}
@@ -54,6 +54,10 @@ function retryAfterSecondsOf(retryAfterHeader) {
54
54
  const retryAt = Date.parse(retryAfterHeader);
55
55
  return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1e3));
56
56
  }
57
+ function toProblemOrNull(toProblem, body, response) {
58
+ const problem = body === null ? null : toProblem(body, response);
59
+ return problem && typeof problem === "object" && !Array.isArray(problem) ? problem : null;
60
+ }
57
61
  async function readProblem(response) {
58
62
  try {
59
63
  const body = JSON.parse(await response.text());
@@ -93,8 +97,20 @@ function createApiClient({
93
97
  csrf = null,
94
98
  timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,
95
99
  onUnauthenticated,
100
+ onRequest,
101
+ onResponse,
102
+ onError,
103
+ toProblem = (body) => body,
104
+ mapResponse = (body) => body,
96
105
  fetch: fetchImplementation
97
106
  } = {}) {
107
+ const notify = (eventName, handler, ...eventArguments) => {
108
+ try {
109
+ handler?.(...eventArguments);
110
+ } catch (error) {
111
+ log.error(`el evento ${eventName} del proyecto tronó`, { message: error?.message });
112
+ }
113
+ };
98
114
  const fetchOf = () => fetchImplementation ?? globalThis.fetch;
99
115
  const csrfCookieName = csrf?.cookieName ?? "XSRF-TOKEN";
100
116
  const csrfHeaderName = csrf?.headerName ?? "X-XSRF-TOKEN";
@@ -137,27 +153,39 @@ function createApiClient({
137
153
  throw new ApiError({ status: API_ERROR_STATUS.offline });
138
154
  }
139
155
  };
140
- const request = async (method, path, {
156
+ const request = async (method, path, options = {}) => {
157
+ const url = apiUrlOf(baseUrl, path, options.params);
158
+ try {
159
+ return await requestOnce(method, url, options);
160
+ } catch (error) {
161
+ if (error instanceof ApiError) {
162
+ notify("onError", onError, error, { method, url });
163
+ }
164
+ throw error;
165
+ }
166
+ };
167
+ const requestOnce = async (method, url, {
141
168
  body,
142
- params,
143
169
  signal,
144
170
  headers,
145
171
  parse = API_RESPONSE_PARSE.json,
146
172
  skipUnauthenticatedHandler = false,
147
173
  timeoutMilliseconds = defaultTimeoutMilliseconds
148
174
  } = {}) => {
149
- const url = apiUrlOf(baseUrl, path, params);
175
+ notify("onRequest", onRequest, { method, url });
176
+ const startedAt = Date.now();
150
177
  let response = await send(method, url, { body, signal, headers, timeoutMilliseconds });
151
178
  if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {
152
179
  log.info("token CSRF vencido: se renueva y se reintenta", { method, url });
153
180
  await send("GET", apiUrlOf(baseUrl, csrf.refreshPath), { signal, timeoutMilliseconds });
154
181
  response = await send(method, url, { body, signal, headers, timeoutMilliseconds });
155
182
  }
183
+ notify("onResponse", onResponse, { method, url, status: response.status, durationMilliseconds: Date.now() - startedAt });
156
184
  if (publishedBuildHeader) {
157
185
  reportPublishedBuild(response.headers.get(publishedBuildHeader));
158
186
  }
159
187
  if (!response.ok) {
160
- const problem = await readProblem(response);
188
+ const problem = toProblemOrNull(toProblem, await readProblem(response), response);
161
189
  const requestedLanguage = headersOf(url, void 0, headers)["Accept-Language"];
162
190
  const responseLanguage = response.headers.get("content-language");
163
191
  const apiError = new ApiError({
@@ -188,12 +216,14 @@ function createApiClient({
188
216
  if (!text) {
189
217
  return null;
190
218
  }
219
+ let json;
191
220
  try {
192
- return JSON.parse(text);
221
+ json = JSON.parse(text);
193
222
  } catch {
194
223
  log.error(`${method} ${url} respondió algo que no es JSON`, { contentType: response.headers.get("content-type") });
195
224
  throw new ApiError({ status: SERVER_ERROR_STATUS });
196
225
  }
226
+ return mapResponse(json, { method, url });
197
227
  };
198
228
  const download = async (path, { params, signal, onDownloadProgress, fallbackFileName = "descarga", timeoutMilliseconds = NO_TIMEOUT } = {}) => {
199
229
  const response = await request("GET", path, { params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds });