@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":"ChangePasswordForm.js","sources":["../../../src/forms/auth/ChangePasswordForm.jsx"],"sourcesContent":["// El formulario de cambiar la contraseña (formik): la actual (si se pide), la nueva con su lista de reglas que se va\n// palomeando, y la nueva otra vez. Sirve para «Mi perfil» y, sin la actual, para la pantalla del enlace del correo\n// («elegir una contraseña nueva»).\n//\n// <ChangePasswordForm onSubmit={(values) => changePassword(values)} />\n// <ChangePasswordForm requireCurrentPassword={false} onSubmit={(values) => resetPassword({token, ...values})} />\n//\n// Al cambiarla, los campos se vacían y el aviso dice que ya cambió (no se queda la contraseña escrita en pantalla).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Stack from '@mui/material/Stack';\n\nimport PasswordChecklist from '@/auth/PasswordChecklist.jsx';\nimport {DEFAULT_PASSWORD_POLICY, isPasswordAccepted} from '@/auth/passwordRules.js';\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';\n\n/**\n * @param {object} props\n * @param {(values: {currentPassword?: string, newPassword: string}) => Promise<unknown>} props.onSubmit cambia la\n * contraseña; si el servidor dice que no (la actual no es), truena con su problema\n * @param {boolean} [props.requireCurrentPassword] por omisión sí; sin ella para la pantalla del enlace del correo\n * @param {object} [props.policy] las reglas de la nueva; por omisión DEFAULT_PASSWORD_POLICY\n */\nexport default function ChangePasswordForm({onSubmit, requireCurrentPassword = true, policy = DEFAULT_PASSWORD_POLICY}) {\n const {t} = useToolkitTranslation('auth');\n const [isDone, setIsDone] = useState(false);\n const schema = useMemo(() => Yup.object({\n currentPassword: requireCurrentPassword ? Yup.string().required() : Yup.string(),\n newPassword: Yup.string().required()\n .test('accepted', {key: 'auth:changePassword.notAccepted'}, (value) => isPasswordAccepted(value, policy))\n .test('different', {key: 'auth:changePassword.sameAsCurrent'}, (value, context) => !requireCurrentPassword || value !== context.parent.currentPassword),\n confirmPassword: Yup.string().required().oneOf([Yup.ref('newPassword')], {key: 'auth:changePassword.mismatch'}),\n }), [policy, requireCurrentPassword]);\n\n const submit = async (values, formikHelpers) => {\n setIsDone(false);\n try {\n await onSubmit(requireCurrentPassword\n ? {currentPassword: values.currentPassword, newPassword: values.newPassword}\n : {newPassword: values.newPassword});\n formikHelpers.resetForm();\n setIsDone(true);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{currentPassword: '', newPassword: '', confirmPassword: ''}} validationSchema={schema} onSubmit={submit}>\n {({values}) => (\n <Form noValidate>\n <Stack spacing={2}>\n {isDone && <Alert severity=\"success\" role=\"status\">{t('changePassword.done')}</Alert>}\n <FormProblemAlert />\n {requireCurrentPassword && (\n <FormTextField name=\"currentPassword\" label={t('changePassword.current')} type=\"password\" autoComplete=\"current-password\" />\n )}\n <FormTextField name=\"newPassword\" label={t('changePassword.next')} type=\"password\" autoComplete=\"new-password\" />\n <PasswordChecklist password={values.newPassword} policy={policy} />\n <FormTextField name=\"confirmPassword\" label={t('changePassword.confirm')} type=\"password\" autoComplete=\"new-password\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('changePassword.submit')}</SubmitButton>\n </Stack>\n </Form>\n )}\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;AA+BA,SAAwB,mBAAmB,EAAC,UAAU,yBAAyB,MAAM,SAAS,2BAA0B;AACpH,QAAM,EAAC,EAAA,IAAK,sBAAsB,MAAM;AACxC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,iBAAiB,yBAAyB,IAAI,OAAA,EAAS,SAAA,IAAa,IAAI,OAAA;AAAA,IACxE,aAAa,IAAI,OAAA,EAAS,WACrB,KAAK,YAAY,EAAC,KAAK,kCAAA,GAAoC,CAAC,UAAU,mBAAmB,OAAO,MAAM,CAAC,EACvG,KAAK,aAAa,EAAC,KAAK,oCAAA,GAAsC,CAAC,OAAO,YAAY,CAAC,0BAA0B,UAAU,QAAQ,OAAO,eAAe;AAAA,IAC1J,iBAAiB,IAAI,SAAS,SAAA,EAAW,MAAM,CAAC,IAAI,IAAI,aAAa,CAAC,GAAG,EAAC,KAAK,gCAA+B;AAAA,EAAA,CACjH,GAAG,CAAC,QAAQ,sBAAsB,CAAC;AAEpC,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,cAAU,KAAK;AACf,QAAI;AACA,YAAM,SAAS,yBACT,EAAC,iBAAiB,OAAO,iBAAiB,aAAa,OAAO,YAAA,IAC9D,EAAC,aAAa,OAAO,aAAY;AACvC,oBAAc,UAAA;AACd,gBAAU,IAAI;AAAA,IAClB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,QAAA,EAAO,eAAe,EAAC,iBAAiB,IAAI,aAAa,IAAI,iBAAiB,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACnH,UAAA,CAAC,EAAC,OAAA,MACC,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACX,UAAA;AAAA,IAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,qBAAqB,EAAA,CAAE;AAAA,wBAC5E,kBAAA,EAAiB;AAAA,IACjB,0BACG,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAE9H,oBAAC,eAAA,EAAc,MAAK,eAAc,OAAO,EAAE,qBAAqB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IAC/G,oBAAC,mBAAA,EAAkB,UAAU,OAAO,aAAa,QAAgB;AAAA,IACjE,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IACtH,oBAAC,gBAAa,IAAI,EAAC,WAAW,aAAA,GAAgB,UAAA,EAAE,uBAAuB,EAAA,CAAE;AAAA,EAAA,EAAA,CAC7E,GACJ,GAER;AAER;"}
1
+ {"version":3,"file":"ChangePasswordForm.js","sources":["../../../src/forms/auth/ChangePasswordForm.jsx"],"sourcesContent":["// El formulario de cambiar la contraseña (formik): la actual (si se pide), la nueva con su lista de reglas que se va\n// palomeando, y la nueva otra vez. Sirve para «Mi perfil» y, sin la actual, para la pantalla del enlace del correo\n// («elegir una contraseña nueva»).\n//\n// <ChangePasswordForm onSubmit={(values) => changePassword(values)} />\n// <ChangePasswordForm requireCurrentPassword={false} onSubmit={(values) => resetPassword({token, ...values})} />\n//\n// Al cambiarla, los campos se vacían y el aviso dice que ya cambió (no se queda la contraseña escrita en pantalla).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Stack from '@mui/material/Stack';\n\nimport PasswordChecklist from '@/auth/PasswordChecklist.jsx';\nimport {DEFAULT_PASSWORD_POLICY, isPasswordAccepted} from '@/auth/passwordRules.js';\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 ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n/**\n * @param {object} props\n * @param {(values: {currentPassword?: string, newPassword: string}) => Promise<unknown>} props.onSubmit cambia la\n * contraseña; si el servidor dice que no (la actual no es), truena con su problema\n * @param {boolean} [props.requireCurrentPassword] por omisión sí; sin ella para la pantalla del enlace del correo\n * @param {object} [props.policy] las reglas de la nueva; por omisión DEFAULT_PASSWORD_POLICY\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ChangePasswordForm({onSubmit, requireCurrentPassword = true, policy = DEFAULT_PASSWORD_POLICY, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const [isDone, setIsDone] = useState(false);\n const schema = useMemo(() => Yup.object({\n currentPassword: requireCurrentPassword ? Yup.string().required() : Yup.string(),\n newPassword: Yup.string().required()\n .test('accepted', {key: 'auth:changePassword.notAccepted'}, (value) => isPasswordAccepted(value, policy))\n .test('different', {key: 'auth:changePassword.sameAsCurrent'}, (value, context) => !requireCurrentPassword || value !== context.parent.currentPassword),\n confirmPassword: Yup.string().required().oneOf([Yup.ref('newPassword')], {key: 'auth:changePassword.mismatch'}),\n }), [policy, requireCurrentPassword]);\n\n const submit = async (values, formikHelpers) => {\n setIsDone(false);\n try {\n await onSubmit(requireCurrentPassword\n ? {currentPassword: values.currentPassword, newPassword: values.newPassword}\n : {newPassword: values.newPassword});\n formikHelpers.resetForm();\n setIsDone(true);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <ComponentTexts namespace=\"auth\" texts={texts}>\n <Formik initialValues={{currentPassword: '', newPassword: '', confirmPassword: ''}} validationSchema={schema} onSubmit={submit}>\n {({values}) => (\n <Form noValidate>\n <Stack spacing={2}>\n {isDone && <Alert severity=\"success\" role=\"status\">{t('changePassword.done')}</Alert>}\n <FormProblemAlert />\n {requireCurrentPassword && (\n <FormTextField name=\"currentPassword\" label={t('changePassword.current')} type=\"password\" autoComplete=\"current-password\" />\n )}\n <FormTextField name=\"newPassword\" label={t('changePassword.next')} type=\"password\" autoComplete=\"new-password\" />\n <PasswordChecklist password={values.newPassword} policy={policy} />\n <FormTextField name=\"confirmPassword\" label={t('changePassword.confirm')} type=\"password\" autoComplete=\"new-password\" />\n <SubmitButton sx={{alignSelf: 'flex-start'}}>{t('changePassword.submit')}</SubmitButton>\n </Stack>\n </Form>\n )}\n </Formik>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAwB,mBAAmB,EAAC,UAAU,yBAAyB,MAAM,SAAS,yBAAyB,SAAQ;AAC3H,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,iBAAiB,yBAAyB,IAAI,OAAA,EAAS,SAAA,IAAa,IAAI,OAAA;AAAA,IACxE,aAAa,IAAI,OAAA,EAAS,WACrB,KAAK,YAAY,EAAC,KAAK,kCAAA,GAAoC,CAAC,UAAU,mBAAmB,OAAO,MAAM,CAAC,EACvG,KAAK,aAAa,EAAC,KAAK,oCAAA,GAAsC,CAAC,OAAO,YAAY,CAAC,0BAA0B,UAAU,QAAQ,OAAO,eAAe;AAAA,IAC1J,iBAAiB,IAAI,SAAS,SAAA,EAAW,MAAM,CAAC,IAAI,IAAI,aAAa,CAAC,GAAG,EAAC,KAAK,gCAA+B;AAAA,EAAA,CACjH,GAAG,CAAC,QAAQ,sBAAsB,CAAC;AAEpC,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,cAAU,KAAK;AACf,QAAI;AACA,YAAM,SAAS,yBACT,EAAC,iBAAiB,OAAO,iBAAiB,aAAa,OAAO,YAAA,IAC9D,EAAC,aAAa,OAAO,aAAY;AACvC,oBAAc,UAAA;AACd,gBAAU,IAAI;AAAA,IAClB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,gBAAA,EAAe,WAAU,QAAO,OAC7B,UAAA,oBAAC,QAAA,EAAO,eAAe,EAAC,iBAAiB,IAAI,aAAa,IAAI,iBAAiB,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACnH,UAAA,CAAC,EAAC,OAAA,MACC,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACX,UAAA;AAAA,IAAA,UAAU,oBAAC,SAAM,UAAS,WAAU,MAAK,UAAU,UAAA,EAAE,qBAAqB,EAAA,CAAE;AAAA,wBAC5E,kBAAA,EAAiB;AAAA,IACjB,0BACG,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAE9H,oBAAC,eAAA,EAAc,MAAK,eAAc,OAAO,EAAE,qBAAqB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IAC/G,oBAAC,mBAAA,EAAkB,UAAU,OAAO,aAAa,QAAgB;AAAA,IACjE,oBAAC,eAAA,EAAc,MAAK,mBAAkB,OAAO,EAAE,wBAAwB,GAAG,MAAK,YAAW,cAAa,eAAA,CAAe;AAAA,IACtH,oBAAC,gBAAa,IAAI,EAAC,WAAW,aAAA,GAAgB,UAAA,EAAE,uBAAuB,EAAA,CAAE;AAAA,EAAA,GAC7E,EAAA,CACJ,GAER,GACJ;AAER;"}
@@ -13,8 +13,8 @@ import FormTextField from "../FormTextField.js";
13
13
  import SubmitButton from "../SubmitButton.js";
14
14
  import { applyServerProblem } from "../applyServerProblem.js";
15
15
  import * as Yup from "yup";
16
- function ForgotPasswordForm({ onSubmit, signInPath }) {
17
- const { t } = useToolkitTranslation("auth");
16
+ function ForgotPasswordForm({ onSubmit, signInPath, texts }) {
17
+ const { t } = useToolkitTranslation("auth", texts);
18
18
  const { Link: RouterLink } = useToolkitNavigation();
19
19
  const [sentToEmail, setSentToEmail] = useState(null);
20
20
  const schema = useMemo(() => Yup.object({ email: Yup.string().trim().required().email() }), []);
@@ -1 +1 @@
1
- {"version":3,"file":"ForgotPasswordForm.js","sources":["../../../src/forms/auth/ForgotPasswordForm.jsx"],"sourcesContent":["// El formulario de «¿Olvidó su contraseña?» (formik): el correo, y al enviarlo, el aviso de que revise su correo en el\n// mismo lugar, con la liga para regresar.\n//\n// OJO, a propósito: el aviso dice «si {{correo}} tiene una cuenta», aunque el sistema sepa si la tiene. Decir «ese correo\n// no existe» le confirma a cualquiera qué correos sí tienen cuenta (enumeración de cuentas); el backend también contesta\n// igual en los dos casos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Alert from '@mui/material/Alert';\nimport AlertTitle from '@mui/material/AlertTitle';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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';\n\n/**\n * @param {object} props\n * @param {(values: {email: string}) => Promise<unknown>} props.onSubmit pide el enlace; si el servidor falla, truena con su\n * problema\n * @param {string} [props.signInPath] la liga para regresar a iniciar sesión\n */\nexport default function ForgotPasswordForm({onSubmit, signInPath}) {\n const {t} = useToolkitTranslation('auth');\n const {Link: RouterLink} = useToolkitNavigation();\n const [sentToEmail, setSentToEmail] = useState(null);\n const schema = useMemo(() => Yup.object({email: Yup.string().trim().required().email()}), []);\n\n const backToSignIn = signInPath && <Link component={RouterLink} to={signInPath} variant=\"body2\">{t('forgotPassword.backToSignIn')}</Link>;\n\n const submit = async (values, formikHelpers) => {\n const email = values.email.trim();\n try {\n await onSubmit({email});\n setSentToEmail(email);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n if (sentToEmail) {\n return (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">\n <AlertTitle>{t('forgotPassword.sentTitle')}</AlertTitle>\n {t('forgotPassword.sent', {email: sentToEmail})}\n </Alert>\n {backToSignIn}\n </Stack>\n );\n }\n\n return (\n <Formik initialValues={{email: ''}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('forgotPassword.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <SubmitButton fullWidth>{t('forgotPassword.submit')}</SubmitButton>\n {backToSignIn}\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AA6BA,SAAwB,mBAAmB,EAAC,UAAU,cAAa;AAC/D,QAAM,EAAC,EAAA,IAAK,sBAAsB,MAAM;AACxC,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,IAAI;AACnD,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO,EAAC,OAAO,IAAI,OAAA,EAAS,KAAA,EAAO,WAAW,MAAA,GAAQ,GAAG,EAAE;AAE5F,QAAM,eAAe,cAAc,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,YAAY,SAAQ,SAAS,UAAA,EAAE,6BAA6B,GAAE;AAElI,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,UAAM,QAAQ,OAAO,MAAM,KAAA;AAC3B,QAAI;AACA,YAAM,SAAS,EAAC,OAAM;AACtB,qBAAe,KAAK;AAAA,IACxB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,MAAI,aAAa;AACb,WACI,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAM,UAAS,WAAU,MAAK,UAC3B,UAAA;AAAA,QAAA,oBAAC,YAAA,EAAY,UAAA,EAAE,0BAA0B,EAAA,CAAE;AAAA,QAC1C,EAAE,uBAAuB,EAAC,OAAO,aAAY;AAAA,MAAA,GAClD;AAAA,MACC;AAAA,IAAA,GACL;AAAA,EAER;AAEA,6BACK,QAAA,EAAO,eAAe,EAAC,OAAO,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACpE,8BAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,sBAAsB,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,wBAC5G,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,uBAAuB,GAAE;AAAA,IACnD;AAAA,EAAA,EAAA,CACL,GACJ,GACJ;AAER;"}
1
+ {"version":3,"file":"ForgotPasswordForm.js","sources":["../../../src/forms/auth/ForgotPasswordForm.jsx"],"sourcesContent":["// El formulario de «¿Olvidó su contraseña?» (formik): el correo, y al enviarlo, el aviso de que revise su correo en el\n// mismo lugar, con la liga para regresar.\n//\n// OJO, a propósito: el aviso dice «si {{correo}} tiene una cuenta», aunque el sistema sepa si la tiene. Decir «ese correo\n// no existe» le confirma a cualquiera qué correos sí tienen cuenta (enumeración de cuentas); el backend también contesta\n// igual en los dos casos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo, useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Alert from '@mui/material/Alert';\nimport AlertTitle from '@mui/material/AlertTitle';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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';\n\n/**\n * @param {object} props\n * @param {(values: {email: string}) => Promise<unknown>} props.onSubmit pide el enlace; si el servidor falla, truena con su\n * problema\n * @param {string} [props.signInPath] la liga para regresar a iniciar sesión\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function ForgotPasswordForm({onSubmit, signInPath, texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n const [sentToEmail, setSentToEmail] = useState(null);\n const schema = useMemo(() => Yup.object({email: Yup.string().trim().required().email()}), []);\n\n const backToSignIn = signInPath && <Link component={RouterLink} to={signInPath} variant=\"body2\">{t('forgotPassword.backToSignIn')}</Link>;\n\n const submit = async (values, formikHelpers) => {\n const email = values.email.trim();\n try {\n await onSubmit({email});\n setSentToEmail(email);\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n if (sentToEmail) {\n return (\n <Stack spacing={2}>\n <Alert severity=\"success\" role=\"status\">\n <AlertTitle>{t('forgotPassword.sentTitle')}</AlertTitle>\n {t('forgotPassword.sent', {email: sentToEmail})}\n </Alert>\n {backToSignIn}\n </Stack>\n );\n }\n\n return (\n <Formik initialValues={{email: ''}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('forgotPassword.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <SubmitButton fullWidth>{t('forgotPassword.submit')}</SubmitButton>\n {backToSignIn}\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AA8BA,SAAwB,mBAAmB,EAAC,UAAU,YAAY,SAAQ;AACtE,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,IAAI;AACnD,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO,EAAC,OAAO,IAAI,OAAA,EAAS,KAAA,EAAO,WAAW,MAAA,GAAQ,GAAG,EAAE;AAE5F,QAAM,eAAe,cAAc,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,YAAY,SAAQ,SAAS,UAAA,EAAE,6BAA6B,GAAE;AAElI,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,UAAM,QAAQ,OAAO,MAAM,KAAA;AAC3B,QAAI;AACA,YAAM,SAAS,EAAC,OAAM;AACtB,qBAAe,KAAK;AAAA,IACxB,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,MAAI,aAAa;AACb,WACI,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAM,UAAS,WAAU,MAAK,UAC3B,UAAA;AAAA,QAAA,oBAAC,YAAA,EAAY,UAAA,EAAE,0BAA0B,EAAA,CAAE;AAAA,QAC1C,EAAE,uBAAuB,EAAC,OAAO,aAAY;AAAA,MAAA,GAClD;AAAA,MACC;AAAA,IAAA,GACL;AAAA,EAER;AAEA,6BACK,QAAA,EAAO,eAAe,EAAC,OAAO,GAAA,GAAK,kBAAkB,QAAQ,UAAU,QACpE,8BAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,sBAAsB,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,wBAC5G,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,uBAAuB,GAAE;AAAA,IACnD;AAAA,EAAA,EAAA,CACL,GACJ,GACJ;AAER;"}
@@ -17,8 +17,8 @@ function RememberMeField({ label }) {
17
17
  const [field] = useField({ name: "remember", type: "checkbox" });
18
18
  return /* @__PURE__ */ jsx(FormControlLabel, { label, control: /* @__PURE__ */ jsx(Checkbox, { ...field }) });
19
19
  }
20
- function SignInForm({ onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = "" }) {
21
- const { t } = useToolkitTranslation("auth");
20
+ function SignInForm({ onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = "", texts }) {
21
+ const { t } = useToolkitTranslation("auth", texts);
22
22
  const { Link: RouterLink } = useToolkitNavigation();
23
23
  const schema = useMemo(() => Yup.object({
24
24
  email: Yup.string().trim().required().email(),
@@ -1 +1 @@
1
- {"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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';\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n */\nexport default function SignInForm({onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = ''}) {\n const {t} = useToolkitTranslation('auth');\n const {Link: RouterLink} = useToolkitNavigation();\n // el esquema se arma al pintar y no al importar: yup toma sus mensajes al armarlo (buildYupLocale ya corrió)\n const schema = useMemo(() => Yup.object({\n email: Yup.string().trim().required().email(),\n password: Yup.string().required(),\n }), []);\n\n const submit = async (values, formikHelpers) => {\n try {\n await onSubmit({...values, email: values.email.trim()});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{email: initialEmail, password: '', remember: false}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('signIn.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <FormTextField name=\"password\" label={t('signIn.password')} type=\"password\" autoComplete=\"current-password\" />\n <Stack direction=\"row\" sx={{alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1}}>\n {showRememberMe ? <RememberMeField label={t('signIn.remember')} /> : <span />}\n {forgotPasswordPath && (\n <Link component={RouterLink} to={forgotPasswordPath} variant=\"body2\">{t('signIn.forgot')}</Link>\n )}\n </Stack>\n <SubmitButton fullWidth>{t('signIn.submit')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,gBAAgB,EAAC,SAAQ;AAC9B,QAAM,CAAC,KAAK,IAAI,SAAS,EAAC,MAAM,YAAY,MAAM,YAAW;AAC7D,SAAO,oBAAC,oBAAiB,OAAc,6BAAU,UAAA,EAAU,GAAG,OAAO,GAAI;AAC7E;AAUA,SAAwB,WAAW,EAAC,UAAU,oBAAoB,iBAAiB,MAAM,eAAe,MAAK;AACzG,QAAM,EAAC,EAAA,IAAK,sBAAsB,MAAM;AACxC,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAE3B,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,OAAO,IAAI,OAAA,EAAS,OAAO,SAAA,EAAW,MAAA;AAAA,IACtC,UAAU,IAAI,OAAA,EAAS,SAAA;AAAA,EAAS,CACnC,GAAG,CAAA,CAAE;AAEN,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,QAAI;AACA,YAAM,SAAS,EAAC,GAAG,QAAQ,OAAO,OAAO,MAAM,KAAA,GAAO;AAAA,IAC1D,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,UAAO,eAAe,EAAC,OAAO,cAAc,UAAU,IAAI,UAAU,MAAA,GAAQ,kBAAkB,QAAQ,UAAU,QAC7G,UAAA,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,cAAc,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,IACrG,oBAAC,eAAA,EAAc,MAAK,YAAW,OAAO,EAAE,iBAAiB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAC5G,qBAAC,OAAA,EAAM,WAAU,OAAM,IAAI,EAAC,YAAY,UAAU,gBAAgB,iBAAiB,UAAU,QAAQ,KAAK,KACrG,UAAA;AAAA,MAAA,iBAAiB,oBAAC,mBAAgB,OAAO,EAAE,iBAAiB,GAAG,wBAAM,QAAA,CAAA,CAAK;AAAA,MAC1E,sBACG,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,oBAAoB,SAAQ,SAAS,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,IAAA,GAEjG;AAAA,wBACC,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,EAAA,EAAA,CAChD,GACJ,GACJ;AAER;"}
1
+ {"version":3,"file":"SignInForm.js","sources":["../../../src/forms/auth/SignInForm.jsx"],"sourcesContent":["// El formulario de iniciar sesión (formik): correo, contraseña, «mantener la sesión abierta» y la liga a «¿Olvidó su\n// contraseña?». La pantalla solo dice qué hacer con los datos:\n//\n// <AuthLayout title={t('auth:signIn.title')}>\n// <SignInForm onSubmit={(values) => signIn(values)} forgotPasswordPath=\"/forgot-password\" />\n// </AuthLayout>\n//\n// Si onSubmit truena con el problema del servidor (RFC 9457; un 401 «correo o contraseña incorrectos», un 429 «espere\n// un minuto»), el aviso sale arriba del formulario y lo de cada campo, en su campo (applyServerProblem).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useMemo} from 'react';\nimport {Form, Formik, useField} from 'formik';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\n\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';\n\n/** «Mantener la sesión abierta», conectado a formik. */\nfunction RememberMeField({label}) {\n const [field] = useField({name: 'remember', type: 'checkbox'});\n return <FormControlLabel label={label} control={<Checkbox {...field} />} />;\n}\n\n/**\n * @param {object} props\n * @param {(values: {email: string, password: string, remember: boolean}) => Promise<unknown>} props.onSubmit entra; si\n * el servidor dice que no, truena con su problema\n * @param {string} [props.forgotPasswordPath] sin ella no sale la liga de «¿Olvidó su contraseña?»\n * @param {boolean} [props.showRememberMe] por omisión sí\n * @param {string} [props.initialEmail]\n * @param {object} [props.texts] los textos que cambian, con la forma de `auth` en los textos del paquete\n */\nexport default function SignInForm({onSubmit, forgotPasswordPath, showRememberMe = true, initialEmail = '', texts}) {\n const {t} = useToolkitTranslation('auth', texts);\n const {Link: RouterLink} = useToolkitNavigation();\n // el esquema se arma al pintar y no al importar: yup toma sus mensajes al armarlo (buildYupLocale ya corrió)\n const schema = useMemo(() => Yup.object({\n email: Yup.string().trim().required().email(),\n password: Yup.string().required(),\n }), []);\n\n const submit = async (values, formikHelpers) => {\n try {\n await onSubmit({...values, email: values.email.trim()});\n } catch (error) {\n applyServerProblem(error, {...formikHelpers, values});\n }\n };\n\n return (\n <Formik initialValues={{email: initialEmail, password: '', remember: false}} validationSchema={schema} onSubmit={submit}>\n <Form noValidate>\n <Stack spacing={2}>\n <FormProblemAlert />\n <FormTextField name=\"email\" label={t('signIn.email')} type=\"email\" autoComplete=\"username\" autoFocus />\n <FormTextField name=\"password\" label={t('signIn.password')} type=\"password\" autoComplete=\"current-password\" />\n <Stack direction=\"row\" sx={{alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1}}>\n {showRememberMe ? <RememberMeField label={t('signIn.remember')} /> : <span />}\n {forgotPasswordPath && (\n <Link component={RouterLink} to={forgotPasswordPath} variant=\"body2\">{t('signIn.forgot')}</Link>\n )}\n </Stack>\n <SubmitButton fullWidth>{t('signIn.submit')}</SubmitButton>\n </Stack>\n </Form>\n </Formik>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,gBAAgB,EAAC,SAAQ;AAC9B,QAAM,CAAC,KAAK,IAAI,SAAS,EAAC,MAAM,YAAY,MAAM,YAAW;AAC7D,SAAO,oBAAC,oBAAiB,OAAc,6BAAU,UAAA,EAAU,GAAG,OAAO,GAAI;AAC7E;AAWA,SAAwB,WAAW,EAAC,UAAU,oBAAoB,iBAAiB,MAAM,eAAe,IAAI,SAAQ;AAChH,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ,KAAK;AAC/C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAE3B,QAAM,SAAS,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpC,OAAO,IAAI,OAAA,EAAS,OAAO,SAAA,EAAW,MAAA;AAAA,IACtC,UAAU,IAAI,OAAA,EAAS,SAAA;AAAA,EAAS,CACnC,GAAG,CAAA,CAAE;AAEN,QAAM,SAAS,OAAO,QAAQ,kBAAkB;AAC5C,QAAI;AACA,YAAM,SAAS,EAAC,GAAG,QAAQ,OAAO,OAAO,MAAM,KAAA,GAAO;AAAA,IAC1D,SAAS,OAAO;AACZ,yBAAmB,OAAO,EAAC,GAAG,eAAe,QAAO;AAAA,IACxD;AAAA,EACJ;AAEA,SACI,oBAAC,UAAO,eAAe,EAAC,OAAO,cAAc,UAAU,IAAI,UAAU,MAAA,GAAQ,kBAAkB,QAAQ,UAAU,QAC7G,UAAA,oBAAC,MAAA,EAAK,YAAU,MACZ,UAAA,qBAAC,OAAA,EAAM,SAAS,GACZ,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB;AAAA,IAClB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,EAAE,cAAc,GAAG,MAAK,SAAQ,cAAa,YAAW,WAAS,KAAA,CAAC;AAAA,IACrG,oBAAC,eAAA,EAAc,MAAK,YAAW,OAAO,EAAE,iBAAiB,GAAG,MAAK,YAAW,cAAa,mBAAA,CAAmB;AAAA,IAC5G,qBAAC,OAAA,EAAM,WAAU,OAAM,IAAI,EAAC,YAAY,UAAU,gBAAgB,iBAAiB,UAAU,QAAQ,KAAK,KACrG,UAAA;AAAA,MAAA,iBAAiB,oBAAC,mBAAgB,OAAO,EAAE,iBAAiB,GAAG,wBAAM,QAAA,CAAA,CAAK;AAAA,MAC1E,sBACG,oBAAC,MAAA,EAAK,WAAW,YAAY,IAAI,oBAAoB,SAAQ,SAAS,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,IAAA,GAEjG;AAAA,wBACC,cAAA,EAAa,WAAS,MAAE,UAAA,EAAE,eAAe,EAAA,CAAE;AAAA,EAAA,EAAA,CAChD,GACJ,GACJ;AAER;"}
@@ -22,8 +22,8 @@ const ACCOUNT_LOCKED_CODE = "account_locked";
22
22
  function isOutOfAttempts(error) {
23
23
  return error?.code === ACCOUNT_LOCKED_CODE || error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts);
24
24
  }
25
- function SecondStepForm({ methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer }) {
26
- const { t } = useToolkitTranslation("security");
25
+ function SecondStepForm({ methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts }) {
26
+ const { t } = useToolkitTranslation("security", texts);
27
27
  const hasApp = methods.includes(SECOND_STEP_METHOD.totp);
28
28
  const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);
29
29
  const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);
@@ -1 +1 @@
1
- {"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\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 {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n */\nexport default function SecondStepForm({methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer}) {\n const {t} = useToolkitTranslation('security');\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <Stack spacing={0.5} sx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </Stack>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SecondStepForm.js","sources":["../../../src/forms/security/SecondStepForm.jsx"],"sourcesContent":["// El segundo paso al entrar: la contraseña fue correcta y la cuenta tiene verificación en dos pasos.\n//\n// Los métodos van en el orden de su fuerza: primero la llave de acceso (no se la roba una página falsa), luego el código de\n// la app, y el de recuperación detrás de un enlace (es para emergencias y se gasta). Cada código equivocado cuenta para el\n// bloqueo de la cuenta: el servidor dice cuántos intentos quedan (problem.remaining_attempts) y se le dice a la persona.\n// Si se acabaron los intentos o se venció el tiempo (un 401 sin intentos, o code account_locked), se regresa a empezar.\n//\n// La pantalla no sabe del servidor: el proyecto le pasa qué hacer.\n//\n// <SecondStepForm methods={['passkey', 'totp', 'recoveryCode']} canUsePasskeys={passkeysSupported()}\n// onUsePasskey={async () => finishWith(await api.post('/auth/second-step/passkey',\n// await signWithPasskey(await api.get('/auth/second-step/passkey-options'))))}\n// onSubmitCode={async ({method, code}) => finishWith(await api.post(`/auth/second-step/${method}`, {code}))}\n// onCancel={(message) => backToSignIn(message)} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Link from '@mui/material/Link';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\n\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 {\n RECOVERY_CODE_MAX_LENGTH, RECOVERY_CODE_MIN_LENGTH, RECOVERY_CODE_PATTERN, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN,\n normalizeRecoveryCode, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Las formas de confirmar el segundo paso, como las manda el servidor. */\nexport const SECOND_STEP_METHOD = Object.freeze({passkey: 'passkey', totp: 'totp', recoveryCode: 'recoveryCode'});\n\nconst UNAUTHENTICATED_STATUS = 401;\nconst ACCOUNT_LOCKED_CODE = 'account_locked';\n\n/**\n * ¿Se acabó la oportunidad (bloqueo o se venció el tiempo)? Entonces hay que empezar de nuevo desde la contraseña.\n *\n * @param {Error & {status?: number, code?: string, problem?: object}} error\n * @returns {boolean}\n */\nfunction isOutOfAttempts(error) {\n return error?.code === ACCOUNT_LOCKED_CODE\n || (error?.status === UNAUTHENTICATED_STATUS && !Number.isInteger(error?.problem?.remaining_attempts));\n}\n\n/**\n * @param {object} props\n * @param {string[]} props.methods los que tiene la cuenta (SECOND_STEP_METHOD), como los mandó el servidor\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys\n * @param {() => Promise<unknown>} [props.onUsePasskey] firma con la llave y termina de entrar; truena si no se pudo\n * @param {(input: {method: string, code: string}) => Promise<unknown>} props.onSubmitCode manda el código ya limpio y\n * termina de entrar; truena con el ApiError si no\n * @param {(message: string|null) => void} props.onCancel regresa a escribir la contraseña (con el motivo, si lo hay)\n * @param {import('react').ReactNode} [props.footer] abajo del botón, dentro del formulario: puede llenar el código con\n * useFormikContext (p. ej. el atajo de los ejemplos)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function SecondStepForm({methods, canUsePasskeys = false, onUsePasskey, onSubmitCode, onCancel, footer, texts}) {\n const {t} = useToolkitTranslation('security', texts);\n const hasApp = methods.includes(SECOND_STEP_METHOD.totp);\n const hasRecovery = methods.includes(SECOND_STEP_METHOD.recoveryCode);\n const hasPasskey = methods.includes(SECOND_STEP_METHOD.passkey) && canUsePasskeys && Boolean(onUsePasskey);\n const [method, setMethod] = useState(hasApp ? SECOND_STEP_METHOD.totp : SECOND_STEP_METHOD.recoveryCode);\n const [passkeyError, setPasskeyError] = useState(null);\n const [isUsingPasskey, setIsUsingPasskey] = useState(false);\n const isApp = method === SECOND_STEP_METHOD.totp;\n\n // el esquema se arma al pintar: yup toma sus mensajes al armarlo\n const schema = isApp\n ? Yup.object({code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat'))})\n : Yup.object({code: Yup.string().trim().required(t('secondStep.recoveryCodeRequired')).min(RECOVERY_CODE_MIN_LENGTH)\n .max(RECOVERY_CODE_MAX_LENGTH).matches(RECOVERY_CODE_PATTERN, t('secondStep.recoveryCodeFormat'))});\n\n const usePasskey = async () => {\n setPasskeyError(null);\n setIsUsingPasskey(true);\n try {\n await onUsePasskey();\n } catch (error) {\n // cancelar el aviso del sistema no es un error que enseñar\n if (!error?.cancelled) {\n setPasskeyError(error?.message);\n }\n } finally {\n setIsUsingPasskey(false);\n }\n };\n\n const submit = async (values, formikHelpers) => {\n formikHelpers.setStatus(null);\n try {\n const code = isApp ? normalizeTotpCode(values.code) : normalizeRecoveryCode(values.code);\n await onSubmitCode({method, code});\n } catch (error) {\n formikHelpers.setFieldValue('code', '', false);\n if (isOutOfAttempts(error)) {\n onCancel(error?.message ?? null);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n // cuántos intentos quedan, junto al mensaje del servidor\n const remainingAttempts = error?.problem?.remaining_attempts;\n if (Number.isInteger(remainingAttempts)) {\n formikHelpers.setStatus(`${error.message} ${t('secondStep.remainingAttempts', {count: remainingAttempts})}`);\n }\n }\n };\n\n return (\n <Stack spacing={2.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('secondStep.intro')}</Typography>\n\n {hasPasskey && (\n <>\n <Button variant=\"contained\" size=\"large\" startIcon={<FingerprintOutlinedIcon />} onClick={usePasskey} disabled={isUsingPasskey}>\n {t('secondStep.usePasskey')}\n </Button>\n {passkeyError && <Alert severity=\"error\" role=\"alert\">{passkeyError}</Alert>}\n {(hasApp || hasRecovery) && <Divider>{t('secondStep.or')}</Divider>}\n </>\n )}\n\n {(hasApp || hasRecovery) && (\n // key: al cambiar de método, el formulario empieza limpio\n <Formik key={method} initialValues={{code: ''}} validationSchema={schema} onSubmit={submit}>\n <Stack component={Form} noValidate spacing={2}>\n <FormProblemAlert />\n {isApp ? (\n <FormTextField name=\"code\" label={t('secondStep.appCodeLabel')} autoFocus autoComplete=\"one-time-code\" clearable={false}\n helperText={t('secondStep.appCodeHelp')}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n ) : (\n <FormTextField name=\"code\" label={t('secondStep.recoveryCodeLabel')} autoFocus autoComplete=\"off\" clearable={false}\n helperText={t('secondStep.recoveryCodeHelp')} slotProps={{htmlInput: {maxLength: RECOVERY_CODE_MAX_LENGTH}}} />\n )}\n <SubmitButton fullWidth>{t('secondStep.confirm')}</SubmitButton>\n {footer}\n </Stack>\n </Formik>\n )}\n {!hasApp && !hasRecovery && footer}\n\n <Stack spacing={0.5} sx={{alignItems: 'center'}}>\n {hasApp && hasRecovery && (\n <Link component=\"button\" type=\"button\" variant=\"body2\"\n onClick={() => setMethod(isApp ? SECOND_STEP_METHOD.recoveryCode : SECOND_STEP_METHOD.totp)}>\n {isApp ? t('secondStep.useRecovery') : t('secondStep.useApp')}\n </Link>\n )}\n <Link component=\"button\" type=\"button\" variant=\"body2\" onClick={() => onCancel(null)}>{t('secondStep.otherAccount')}</Link>\n </Stack>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { Formik, Form } from "formik";
5
5
  import Alert from "@mui/material/Alert";
@@ -30,12 +30,13 @@ import SubmitButton from "../SubmitButton.js";
30
30
  import { applyServerProblem } from "../applyServerProblem.js";
31
31
  import * as Yup from "yup";
32
32
  import { PASSKEY_NAME_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_PATTERN, normalizeTotpCode, CURRENT_PASSWORD_MAX_LENGTH } from "./securityRules.js";
33
- import { formatDateTime } from "../../formatting.js";
33
+ import { useFormatters } from "../../toolkit/useFormatters.js";
34
34
  import RecoveryCodesDialog from "../../security/RecoveryCodesDialog.js";
35
35
  import { useToolkitTranslation } from "../../toolkit/useToolkitTranslation.js";
36
36
  import CopyButton from "../../ui/actions/CopyButton.js";
37
37
  import Tag from "../../ui/labels/Tag.js";
38
38
  import SectionCard from "../../ui/layout/SectionCard.js";
39
+ import ComponentTexts from "../../toolkit/ComponentTexts.js";
39
40
  const FEW_CODES_LEFT = 2;
40
41
  const QR_SIZE_PIXELS = 200;
41
42
  const CODE_FONT = "ui-monospace, SFMono-Regular, Menlo, monospace";
@@ -189,8 +190,9 @@ function SettingRow({ Icon, title, caption, captionColor = "text.secondary", act
189
190
  action
190
191
  ] });
191
192
  }
192
- function TwoStepVerificationCard({ security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice }) {
193
- const { t } = useToolkitTranslation("security");
193
+ function TwoStepVerificationCard({ security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts }) {
194
+ const { t } = useToolkitTranslation("security", texts);
195
+ const { formatDateTime } = useFormatters();
194
196
  const [dialog, setDialog] = useState(null);
195
197
  const [totpSetup, setTotpSetup] = useState(null);
196
198
  const [recoveryCodes, setRecoveryCodes] = useState(null);
@@ -216,7 +218,7 @@ function TwoStepVerificationCard({ security, onSecurityChange, actions, canUsePa
216
218
  };
217
219
  const passkeyBeingRemoved = dialog?.passkey;
218
220
  const codesLeft = security.recoveryCodesRemaining;
219
- return /* @__PURE__ */ jsxs(SectionCard, { title: t("twoStep.title"), children: [
221
+ return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "security", texts, children: /* @__PURE__ */ jsxs(SectionCard, { title: t("twoStep.title"), children: [
220
222
  /* @__PURE__ */ jsxs(Stack, { spacing: 2, children: [
221
223
  /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: t("twoStep.description") }),
222
224
  /* @__PURE__ */ jsx(Box, { children: security.isEnabled ? /* @__PURE__ */ jsx(Tag, { label: t("twoStep.active"), color: "success", tooltip: t("twoStep.activeSince", { date: formatDateTime(security.enabledAt) }) }) : /* @__PURE__ */ jsx(Tag, { label: t("twoStep.inactive"), color: "warning", tooltip: t("twoStep.inactiveTooltip") }) }),
@@ -328,7 +330,7 @@ function TwoStepVerificationCard({ security, onSecurityChange, actions, canUsePa
328
330
  }
329
331
  ),
330
332
  /* @__PURE__ */ jsx(RecoveryCodesDialog, { codes: recoveryCodes, account, onClose: () => setRecoveryCodes(null) })
331
- ] });
333
+ ] }) });
332
334
  }
333
335
  export {
334
336
  TwoStepVerificationCard as default
@@ -1 +1 @@
1
- {"version":3,"file":"TwoStepVerificationCard.js","sources":["../../../src/forms/security/TwoStepVerificationCard.jsx"],"sourcesContent":["// «Verificación en dos pasos» de «Mi perfil»: las llaves de acceso, la app autenticadora y los códigos de recuperación.\n//\n// Lo que QUITA protección (desactivar la app, quitar una llave, generar códigos nuevos) pide la contraseña: una sesión que\n// alguien dejó abierta no debe bastar para desarmar la cuenta. Activar no la pide.\n//\n// La tarjeta no sabe del servidor: el proyecto le pasa el estado (security) y qué hacer en cada caso (actions); cada\n// acción regresa {security, recoveryCodes?} y la tarjeta avisa el estado nuevo con onSecurityChange. Las llaves de acceso\n// las crea el proyecto con createPasskey (@stackcraftmx/calcita/passkeys) dentro de actions.addPasskey: así esta puerta no\n// carga la librería de llaves.\n//\n// <TwoStepVerificationCard security={security} onSecurityChange={setSecurity} account={session.email}\n// canUsePasskeys={passkeysSupported()} actions={securityActions} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\nimport PhonelinkLockOutlinedIcon from '@mui/icons-material/PhonelinkLockOutlined';\nimport VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';\n\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 {\n CURRENT_PASSWORD_MAX_LENGTH, PASSKEY_NAME_MAX_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {formatDateTime} from '@/formatting.js';\nimport RecoveryCodesDialog from '@/security/RecoveryCodesDialog.jsx';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CopyButton from '@/ui/actions/CopyButton.jsx';\nimport Tag from '@/ui/labels/Tag.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\n\n// los códigos que quedan desde donde se avisa en ámbar que ya son pocos\nconst FEW_CODES_LEFT = 2;\nconst QR_SIZE_PIXELS = 200;\nconst CODE_FONT = 'ui-monospace, SFMono-Regular, Menlo, monospace';\n\n/**\n * Lo que hace un formulario de la tarjeta con un error: un ApiError va a su campo o arriba (applyServerProblem); un\n * error del navegador (la llave de acceso) va arriba; si la persona solo canceló, nada.\n *\n * @param {Error & {cancelled?: boolean, status?: number}} error\n * @param {object} formikHelpers\n * @param {object} values\n */\nfunction showFormError(error, formikHelpers, values) {\n if (error?.cancelled) {\n return;\n }\n if (error?.status === undefined) {\n formikHelpers.setStatus(error?.message);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n}\n\n/** Confirmar con la contraseña antes de quitar protección. */\nfunction PasswordDialog({open, title, description, confirmLabel, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const passwordSchema = Yup.object({password: Yup.string().max(CURRENT_PASSWORD_MAX_LENGTH).required(t('twoStep.passwordRequired'))});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{password: ''}} validationSchema={passwordSchema} enableReinitialize\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(values.password);\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{description}</DialogContentText>\n <FormProblemAlert />\n <FormTextField name=\"password\" label={t('twoStep.passwordLabel')} type=\"password\" autoComplete=\"current-password\" autoFocus />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" color=\"error\">{confirmLabel}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Activar la app: escanear el QR (o teclear la clave) y confirmar con un código. */\nfunction TotpDialog({setup, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const codeSchema = Yup.object({\n code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat')),\n });\n\n return (\n <Dialog open={Boolean(setup)} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{code: ''}} validationSchema={codeSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(normalizeTotpCode(values.code));\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.totpDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{t('twoStep.totpStep1')}</Typography>\n {setup?.qrImageUrl && (\n <Box component=\"img\" src={setup.qrImageUrl} alt={t('twoStep.totpQrAlt')}\n sx={{width: QR_SIZE_PIXELS, height: QR_SIZE_PIXELS, alignSelf: 'center', borderRadius: 1, bgcolor: 'common.white'}} />\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">{t('twoStep.totpCantScan')}</Typography>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Box component=\"code\" sx={{fontFamily: CODE_FONT, fontSize: 13, wordBreak: 'break-all'}}>{setup?.secret}</Box>\n <CopyButton value={setup?.secret ?? ''} />\n </Stack>\n <Typography variant=\"body2\">{t('twoStep.totpStep2')}</Typography>\n <FormProblemAlert />\n <FormTextField name=\"code\" label={t('twoStep.codeLabel')} autoComplete=\"one-time-code\" clearable={false}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n {setup?.extra}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\">{t('twoStep.activate')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Nombrar y crear una llave nueva (el navegador pide la huella, el rostro o el PIN); notice va arriba del nombre. */\nfunction PasskeyDialog({open, onAdd, onClose, notice}) {\n const {t} = useToolkitTranslation('security');\n const nameSchema = Yup.object({name: Yup.string().trim().max(PASSKEY_NAME_MAX_LENGTH)});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{name: ''}} validationSchema={nameSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onAdd(values.name.trim());\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.passkeyDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{t('twoStep.passkeyDialogText')}</DialogContentText>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('twoStep.passkeyName')} placeholder={t('twoStep.passkeyNamePlaceholder')} autoFocus\n slotProps={{htmlInput: {maxLength: PASSKEY_NAME_MAX_LENGTH}}} />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" startIcon={<FingerprintOutlinedIcon />}>{t('twoStep.createPasskey')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/**\n * Un renglón de la tarjeta: ícono, nombre, explicación chica y su botón a la derecha.\n *\n * @param {{Icon: import('react').ElementType, title: string, caption?: import('react').ReactNode, captionColor?: string, action?: import('react').ReactNode}} props\n */\nfunction SettingRow({Icon, title, caption, captionColor = 'text.secondary', action}) {\n return (\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center', justifyContent: 'space-between'}}>\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'center', minWidth: 0}}>\n <Icon color=\"action\" />\n <Box sx={{minWidth: 0}}>\n <Typography variant=\"subtitle2\">{title}</Typography>\n {caption && <Typography variant=\"caption\" color={captionColor} component=\"p\">{caption}</Typography>}\n </Box>\n </Stack>\n {action}\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}} props.security\n * @param {(security: {isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}) => void} props.onSecurityChange\n * @param {object} props.actions lo que hace el proyecto en cada caso; cada una regresa {security, recoveryCodes?}\n * @param {(name: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.addPasskey crea la llave\n * (createPasskey) y la guarda en el servidor\n * @param {(passkey: {id: string, name: string}, password: string) => Promise<{security: object}>} props.actions.removePasskey\n * @param {() => Promise<{secret: string, qrImageUrl: string, extra?: import('react').ReactNode}>} props.actions.startTotp\n * la clave y su QR; extra va abajo del campo (p. ej. un atajo de los ejemplos)\n * @param {(code: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.confirmTotp\n * @param {(password: string) => Promise<{security: object}>} props.actions.disableTotp\n * @param {(password: string) => Promise<{security: object, recoveryCodes: string[]}>} props.actions.regenerateRecoveryCodes\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys; sin él no se ofrece agregar llaves\n * @param {string} [props.account] la cuenta (el correo), para el archivo de los códigos\n * @param {import('react').ReactNode} [props.passkeyNotice] un aviso en el diálogo de crear la llave, antes de que el\n * navegador la guarde (p. ej. el de la demostración: la llave queda de verdad en el llavero de la persona)\n */\nexport default function TwoStepVerificationCard({security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice}) {\n const {t} = useToolkitTranslation('security');\n // qué diálogo está abierto: uno a la vez\n const [dialog, setDialog] = useState(null);\n const [totpSetup, setTotpSetup] = useState(null);\n const [recoveryCodes, setRecoveryCodes] = useState(null);\n const [error, setError] = useState(null);\n\n const close = () => setDialog(null);\n // lo que regresa cada acción: el estado nuevo, los códigos si hubo, el aviso y cerrar el diálogo\n const apply = (result, message) => {\n onSecurityChange(result.security);\n if (result.recoveryCodes?.length) {\n setRecoveryCodes(result.recoveryCodes);\n }\n if (message) {\n toastSuccess(message);\n }\n close();\n };\n\n const startTotp = async () => {\n setError(null);\n try {\n setTotpSetup(await actions.startTotp());\n } catch (caught) {\n setError(caught.message);\n }\n };\n\n const passkeyBeingRemoved = dialog?.passkey;\n const codesLeft = security.recoveryCodesRemaining;\n\n return (\n <SectionCard title={t('twoStep.title')}>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('twoStep.description')}</Typography>\n <Box>\n {security.isEnabled\n ? <Tag label={t('twoStep.active')} color=\"success\" tooltip={t('twoStep.activeSince', {date: formatDateTime(security.enabledAt)})} />\n : <Tag label={t('twoStep.inactive')} color=\"warning\" tooltip={t('twoStep.inactiveTooltip')} />}\n </Box>\n {error && <Alert severity=\"error\" onClose={() => setError(null)}>{error}</Alert>}\n\n <Divider />\n <SettingRow Icon={FingerprintOutlinedIcon} title={t('twoStep.passkeysTitle')}\n caption={security.passkeys.length === 0 ? (canUsePasskeys ? t('twoStep.passkeysEmpty') : t('passkeys.unsupported')) : null}\n action={canUsePasskeys && <Button size=\"small\" onClick={() => setDialog({kind: 'passkey'})}>{t('twoStep.add')}</Button>} />\n {security.passkeys.length > 0 && (\n <List dense disablePadding>\n {security.passkeys.map((passkey) => (\n <ListItem key={passkey.id} disableGutters secondaryAction={(\n <Tooltip title={t('twoStep.removePasskeyTooltip')}>\n <IconButton edge=\"end\" aria-label={t('twoStep.removePasskey', {name: passkey.name})}\n onClick={() => setDialog({kind: 'removePasskey', passkey})}>\n <DeleteOutlineOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}>\n <ListItemIcon sx={{minWidth: 32}}><VpnKeyOutlinedIcon fontSize=\"small\" /></ListItemIcon>\n <ListItemText primary={passkey.name}\n secondary={passkey.lastUsedAt\n ? t('twoStep.passkeyUsed', {date: formatDateTime(passkey.lastUsedAt)})\n : t('twoStep.passkeyCreated', {date: formatDateTime(passkey.createdAt)})} />\n </ListItem>\n ))}\n </List>\n )}\n\n <Divider />\n <SettingRow Icon={PhonelinkLockOutlinedIcon} title={t('twoStep.appTitle')}\n caption={security.totp.isEnabled ? t('twoStep.appActiveSince', {date: formatDateTime(security.totp.enabledAt)}) : t('twoStep.appDescription')}\n action={security.totp.isEnabled\n ? <Button size=\"small\" color=\"error\" onClick={() => setDialog({kind: 'disableTotp'})}>{t('twoStep.deactivate')}</Button>\n : <Button size=\"small\" onClick={startTotp}>{t('twoStep.activate')}</Button>} />\n\n {/* los códigos de recuperación solo existen con la verificación activa */}\n {security.isEnabled && (\n <>\n <Divider />\n <SettingRow Icon={VpnKeyOutlinedIcon} title={t('twoStep.codesTitle')}\n caption={codesLeft ? t('twoStep.codesRemaining', {count: codesLeft}) : t('twoStep.codesNone')}\n captionColor={codesLeft <= FEW_CODES_LEFT ? 'warning.main' : 'text.secondary'}\n action={<Button size=\"small\" onClick={() => setDialog({kind: 'regenerateCodes'})}>{t('twoStep.regenerate')}</Button>} />\n </>\n )}\n </Stack>\n\n <PasskeyDialog open={dialog?.kind === 'passkey'} onClose={close} notice={passkeyNotice}\n onAdd={async (name) => apply(await actions.addPasskey(name), t('twoStep.passkeyAdded'))} />\n <TotpDialog setup={totpSetup} onClose={() => setTotpSetup(null)}\n onConfirm={async (code) => {\n const result = await actions.confirmTotp(code);\n setTotpSetup(null);\n apply(result, t('twoStep.appActivated'));\n }} />\n <PasswordDialog open={dialog?.kind === 'disableTotp'} onClose={close} title={t('twoStep.disableTitle')}\n description={t('twoStep.disableText')} confirmLabel={t('twoStep.deactivate')}\n onConfirm={async (password) => apply(await actions.disableTotp(password), t('twoStep.appDeactivated'))} />\n <PasswordDialog open={dialog?.kind === 'removePasskey'} onClose={close}\n title={t('twoStep.removeTitle', {name: passkeyBeingRemoved?.name ?? ''})} description={t('twoStep.removeText')}\n confirmLabel={t('twoStep.remove')}\n onConfirm={async (password) => apply(await actions.removePasskey(passkeyBeingRemoved, password), t('twoStep.passkeyRemoved'))} />\n <PasswordDialog open={dialog?.kind === 'regenerateCodes'} onClose={close} title={t('twoStep.regenerateTitle')}\n description={t('twoStep.regenerateText')} confirmLabel={t('twoStep.generate')}\n onConfirm={async (password) => apply(await actions.regenerateRecoveryCodes(password))} />\n <RecoveryCodesDialog codes={recoveryCodes} account={account} onClose={() => setRecoveryCodes(null)} />\n </SectionCard>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TwoStepVerificationCard.js","sources":["../../../src/forms/security/TwoStepVerificationCard.jsx"],"sourcesContent":["// «Verificación en dos pasos» de «Mi perfil»: las llaves de acceso, la app autenticadora y los códigos de recuperación.\n//\n// Lo que QUITA protección (desactivar la app, quitar una llave, generar códigos nuevos) pide la contraseña: una sesión que\n// alguien dejó abierta no debe bastar para desarmar la cuenta. Activar no la pide.\n//\n// La tarjeta no sabe del servidor: el proyecto le pasa el estado (security) y qué hacer en cada caso (actions); cada\n// acción regresa {security, recoveryCodes?} y la tarjeta avisa el estado nuevo con onSecurityChange. Las llaves de acceso\n// las crea el proyecto con createPasskey (@stackcraftmx/calcita/passkeys) dentro de actions.addPasskey: así esta puerta no\n// carga la librería de llaves.\n//\n// <TwoStepVerificationCard security={security} onSecurityChange={setSecurity} account={session.email}\n// canUsePasskeys={passkeysSupported()} actions={securityActions} />\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {Form, Formik} from 'formik';\nimport Alert from '@mui/material/Alert';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';\nimport FingerprintOutlinedIcon from '@mui/icons-material/FingerprintOutlined';\nimport PhonelinkLockOutlinedIcon from '@mui/icons-material/PhonelinkLockOutlined';\nimport VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';\n\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 {\n CURRENT_PASSWORD_MAX_LENGTH, PASSKEY_NAME_MAX_LENGTH, TOTP_CODE_MAX_LENGTH, TOTP_CODE_MIN_LENGTH, TOTP_CODE_PATTERN, normalizeTotpCode,\n} from '@/forms/security/securityRules.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport RecoveryCodesDialog from '@/security/RecoveryCodesDialog.jsx';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport CopyButton from '@/ui/actions/CopyButton.jsx';\nimport Tag from '@/ui/labels/Tag.jsx';\nimport SectionCard from '@/ui/layout/SectionCard.jsx';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n// los códigos que quedan desde donde se avisa en ámbar que ya son pocos\nconst FEW_CODES_LEFT = 2;\nconst QR_SIZE_PIXELS = 200;\nconst CODE_FONT = 'ui-monospace, SFMono-Regular, Menlo, monospace';\n\n/**\n * Lo que hace un formulario de la tarjeta con un error: un ApiError va a su campo o arriba (applyServerProblem); un\n * error del navegador (la llave de acceso) va arriba; si la persona solo canceló, nada.\n *\n * @param {Error & {cancelled?: boolean, status?: number}} error\n * @param {object} formikHelpers\n * @param {object} values\n */\nfunction showFormError(error, formikHelpers, values) {\n if (error?.cancelled) {\n return;\n }\n if (error?.status === undefined) {\n formikHelpers.setStatus(error?.message);\n return;\n }\n applyServerProblem(error, {...formikHelpers, values});\n}\n\n/** Confirmar con la contraseña antes de quitar protección. */\nfunction PasswordDialog({open, title, description, confirmLabel, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const passwordSchema = Yup.object({password: Yup.string().max(CURRENT_PASSWORD_MAX_LENGTH).required(t('twoStep.passwordRequired'))});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{password: ''}} validationSchema={passwordSchema} enableReinitialize\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(values.password);\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{title}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{description}</DialogContentText>\n <FormProblemAlert />\n <FormTextField name=\"password\" label={t('twoStep.passwordLabel')} type=\"password\" autoComplete=\"current-password\" autoFocus />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" color=\"error\">{confirmLabel}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Activar la app: escanear el QR (o teclear la clave) y confirmar con un código. */\nfunction TotpDialog({setup, onConfirm, onClose}) {\n const {t} = useToolkitTranslation('security');\n const codeSchema = Yup.object({\n code: Yup.string().trim().required(t('twoStep.codeRequired')).min(TOTP_CODE_MIN_LENGTH).max(TOTP_CODE_MAX_LENGTH)\n .matches(TOTP_CODE_PATTERN, t('twoStep.codeFormat')),\n });\n\n return (\n <Dialog open={Boolean(setup)} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{code: ''}} validationSchema={codeSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onConfirm(normalizeTotpCode(values.code));\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.totpDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{t('twoStep.totpStep1')}</Typography>\n {setup?.qrImageUrl && (\n <Box component=\"img\" src={setup.qrImageUrl} alt={t('twoStep.totpQrAlt')}\n sx={{width: QR_SIZE_PIXELS, height: QR_SIZE_PIXELS, alignSelf: 'center', borderRadius: 1, bgcolor: 'common.white'}} />\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">{t('twoStep.totpCantScan')}</Typography>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Box component=\"code\" sx={{fontFamily: CODE_FONT, fontSize: 13, wordBreak: 'break-all'}}>{setup?.secret}</Box>\n <CopyButton value={setup?.secret ?? ''} />\n </Stack>\n <Typography variant=\"body2\">{t('twoStep.totpStep2')}</Typography>\n <FormProblemAlert />\n <FormTextField name=\"code\" label={t('twoStep.codeLabel')} autoComplete=\"one-time-code\" clearable={false}\n slotProps={{htmlInput: {inputMode: 'numeric', maxLength: TOTP_CODE_MAX_LENGTH}}} />\n {setup?.extra}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\">{t('twoStep.activate')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/** Nombrar y crear una llave nueva (el navegador pide la huella, el rostro o el PIN); notice va arriba del nombre. */\nfunction PasskeyDialog({open, onAdd, onClose, notice}) {\n const {t} = useToolkitTranslation('security');\n const nameSchema = Yup.object({name: Yup.string().trim().max(PASSKEY_NAME_MAX_LENGTH)});\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <Formik initialValues={{name: ''}} validationSchema={nameSchema}\n onSubmit={async (values, helpers) => {\n try {\n await onAdd(values.name.trim());\n } catch (error) {\n showFormError(error, helpers, values);\n }\n }}>\n {() => (\n <Form noValidate>\n <DialogTitle>{t('twoStep.passkeyDialogTitle')}</DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <DialogContentText>{t('twoStep.passkeyDialogText')}</DialogContentText>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n <FormProblemAlert />\n <FormTextField name=\"name\" label={t('twoStep.passkeyName')} placeholder={t('twoStep.passkeyNamePlaceholder')} autoFocus\n slotProps={{htmlInput: {maxLength: PASSKEY_NAME_MAX_LENGTH}}} />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{t('common:actions.cancel')}</Button>\n <SubmitButton size=\"medium\" startIcon={<FingerprintOutlinedIcon />}>{t('twoStep.createPasskey')}</SubmitButton>\n </DialogActions>\n </Form>\n )}\n </Formik>\n </Dialog>\n );\n}\n\n/**\n * Un renglón de la tarjeta: ícono, nombre, explicación chica y su botón a la derecha.\n *\n * @param {{Icon: import('react').ElementType, title: string, caption?: import('react').ReactNode, captionColor?: string, action?: import('react').ReactNode}} props\n */\nfunction SettingRow({Icon, title, caption, captionColor = 'text.secondary', action}) {\n return (\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center', justifyContent: 'space-between'}}>\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'center', minWidth: 0}}>\n <Icon color=\"action\" />\n <Box sx={{minWidth: 0}}>\n <Typography variant=\"subtitle2\">{title}</Typography>\n {caption && <Typography variant=\"caption\" color={captionColor} component=\"p\">{caption}</Typography>}\n </Box>\n </Stack>\n {action}\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}} props.security\n * @param {(security: {isEnabled: boolean, enabledAt?: string|null, passkeys: Array<{id: string, name: string, createdAt: string, lastUsedAt?: string|null}>, totp: {isEnabled: boolean, enabledAt?: string|null}, recoveryCodesRemaining: number}) => void} props.onSecurityChange\n * @param {object} props.actions lo que hace el proyecto en cada caso; cada una regresa {security, recoveryCodes?}\n * @param {(name: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.addPasskey crea la llave\n * (createPasskey) y la guarda en el servidor\n * @param {(passkey: {id: string, name: string}, password: string) => Promise<{security: object}>} props.actions.removePasskey\n * @param {() => Promise<{secret: string, qrImageUrl: string, extra?: import('react').ReactNode}>} props.actions.startTotp\n * la clave y su QR; extra va abajo del campo (p. ej. un atajo de los ejemplos)\n * @param {(code: string) => Promise<{security: object, recoveryCodes?: string[]}>} props.actions.confirmTotp\n * @param {(password: string) => Promise<{security: object}>} props.actions.disableTotp\n * @param {(password: string) => Promise<{security: object, recoveryCodes: string[]}>} props.actions.regenerateRecoveryCodes\n * @param {boolean} [props.canUsePasskeys] passkeysSupported() de /passkeys; sin él no se ofrece agregar llaves\n * @param {string} [props.account] la cuenta (el correo), para el archivo de los códigos\n * @param {import('react').ReactNode} [props.passkeyNotice] un aviso en el diálogo de crear la llave, antes de que el\n * navegador la guarde (p. ej. el de la demostración: la llave queda de verdad en el llavero de la persona)\n * @param {object} [props.texts] los textos que cambian, con la forma de `security` en los textos del paquete\n */\nexport default function TwoStepVerificationCard({security, onSecurityChange, actions, canUsePasskeys = false, account, passkeyNotice, texts}) {\n const {t} = useToolkitTranslation('security', texts);\n const {formatDateTime} = useFormatters();\n // qué diálogo está abierto: uno a la vez\n const [dialog, setDialog] = useState(null);\n const [totpSetup, setTotpSetup] = useState(null);\n const [recoveryCodes, setRecoveryCodes] = useState(null);\n const [error, setError] = useState(null);\n\n const close = () => setDialog(null);\n // lo que regresa cada acción: el estado nuevo, los códigos si hubo, el aviso y cerrar el diálogo\n const apply = (result, message) => {\n onSecurityChange(result.security);\n if (result.recoveryCodes?.length) {\n setRecoveryCodes(result.recoveryCodes);\n }\n if (message) {\n toastSuccess(message);\n }\n close();\n };\n\n const startTotp = async () => {\n setError(null);\n try {\n setTotpSetup(await actions.startTotp());\n } catch (caught) {\n setError(caught.message);\n }\n };\n\n const passkeyBeingRemoved = dialog?.passkey;\n const codesLeft = security.recoveryCodesRemaining;\n\n return (\n <ComponentTexts namespace=\"security\" texts={texts}>\n <SectionCard title={t('twoStep.title')}>\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">{t('twoStep.description')}</Typography>\n <Box>\n {security.isEnabled\n ? <Tag label={t('twoStep.active')} color=\"success\" tooltip={t('twoStep.activeSince', {date: formatDateTime(security.enabledAt)})} />\n : <Tag label={t('twoStep.inactive')} color=\"warning\" tooltip={t('twoStep.inactiveTooltip')} />}\n </Box>\n {error && <Alert severity=\"error\" onClose={() => setError(null)}>{error}</Alert>}\n\n <Divider />\n <SettingRow Icon={FingerprintOutlinedIcon} title={t('twoStep.passkeysTitle')}\n caption={security.passkeys.length === 0 ? (canUsePasskeys ? t('twoStep.passkeysEmpty') : t('passkeys.unsupported')) : null}\n action={canUsePasskeys && <Button size=\"small\" onClick={() => setDialog({kind: 'passkey'})}>{t('twoStep.add')}</Button>} />\n {security.passkeys.length > 0 && (\n <List dense disablePadding>\n {security.passkeys.map((passkey) => (\n <ListItem key={passkey.id} disableGutters secondaryAction={(\n <Tooltip title={t('twoStep.removePasskeyTooltip')}>\n <IconButton edge=\"end\" aria-label={t('twoStep.removePasskey', {name: passkey.name})}\n onClick={() => setDialog({kind: 'removePasskey', passkey})}>\n <DeleteOutlineOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}>\n <ListItemIcon sx={{minWidth: 32}}><VpnKeyOutlinedIcon fontSize=\"small\" /></ListItemIcon>\n <ListItemText primary={passkey.name}\n secondary={passkey.lastUsedAt\n ? t('twoStep.passkeyUsed', {date: formatDateTime(passkey.lastUsedAt)})\n : t('twoStep.passkeyCreated', {date: formatDateTime(passkey.createdAt)})} />\n </ListItem>\n ))}\n </List>\n )}\n\n <Divider />\n <SettingRow Icon={PhonelinkLockOutlinedIcon} title={t('twoStep.appTitle')}\n caption={security.totp.isEnabled ? t('twoStep.appActiveSince', {date: formatDateTime(security.totp.enabledAt)}) : t('twoStep.appDescription')}\n action={security.totp.isEnabled\n ? <Button size=\"small\" color=\"error\" onClick={() => setDialog({kind: 'disableTotp'})}>{t('twoStep.deactivate')}</Button>\n : <Button size=\"small\" onClick={startTotp}>{t('twoStep.activate')}</Button>} />\n\n {/* los códigos de recuperación solo existen con la verificación activa */}\n {security.isEnabled && (\n <>\n <Divider />\n <SettingRow Icon={VpnKeyOutlinedIcon} title={t('twoStep.codesTitle')}\n caption={codesLeft ? t('twoStep.codesRemaining', {count: codesLeft}) : t('twoStep.codesNone')}\n captionColor={codesLeft <= FEW_CODES_LEFT ? 'warning.main' : 'text.secondary'}\n action={<Button size=\"small\" onClick={() => setDialog({kind: 'regenerateCodes'})}>{t('twoStep.regenerate')}</Button>} />\n </>\n )}\n </Stack>\n\n <PasskeyDialog open={dialog?.kind === 'passkey'} onClose={close} notice={passkeyNotice}\n onAdd={async (name) => apply(await actions.addPasskey(name), t('twoStep.passkeyAdded'))} />\n <TotpDialog setup={totpSetup} onClose={() => setTotpSetup(null)}\n onConfirm={async (code) => {\n const result = await actions.confirmTotp(code);\n setTotpSetup(null);\n apply(result, t('twoStep.appActivated'));\n }} />\n <PasswordDialog open={dialog?.kind === 'disableTotp'} onClose={close} title={t('twoStep.disableTitle')}\n description={t('twoStep.disableText')} confirmLabel={t('twoStep.deactivate')}\n onConfirm={async (password) => apply(await actions.disableTotp(password), t('twoStep.appDeactivated'))} />\n <PasswordDialog open={dialog?.kind === 'removePasskey'} onClose={close}\n title={t('twoStep.removeTitle', {name: passkeyBeingRemoved?.name ?? ''})} description={t('twoStep.removeText')}\n confirmLabel={t('twoStep.remove')}\n onConfirm={async (password) => apply(await actions.removePasskey(passkeyBeingRemoved, password), t('twoStep.passkeyRemoved'))} />\n <PasswordDialog open={dialog?.kind === 'regenerateCodes'} onClose={close} title={t('twoStep.regenerateTitle')}\n description={t('twoStep.regenerateText')} confirmLabel={t('twoStep.generate')}\n onConfirm={async (password) => apply(await actions.regenerateRecoveryCodes(password))} />\n <RecoveryCodesDialog codes={recoveryCodes} account={account} onClose={() => setRecoveryCodes(null)} />\n </SectionCard>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { useFormContext } from "react-hook-form";
5
+ import FormProblemAlert from "./FormProblemAlert.js";
6
+ import SubmitButton from "./SubmitButton.js";
7
+ import WizardLayout from "../inputs/WizardLayout.js";
8
+ function FormWizard({ steps, submitLabel, texts }) {
9
+ const { trigger, resetField, getFieldState, formState } = useFormContext();
10
+ const { isSubmitting, submitCount } = formState;
11
+ const [activeIndex, setActiveIndex] = useState(0);
12
+ const [skippedKeys, setSkippedKeys] = useState(() => /* @__PURE__ */ new Set());
13
+ const wasSubmitting = useRef(false);
14
+ const activeStep = steps[activeIndex];
15
+ useEffect(() => {
16
+ if (wasSubmitting.current && !isSubmitting) {
17
+ const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));
18
+ if (errorStepIndex >= 0) {
19
+ setActiveIndex(errorStepIndex);
20
+ }
21
+ }
22
+ wasSubmitting.current = isSubmitting;
23
+ }, [isSubmitting, submitCount]);
24
+ const goNext = async () => {
25
+ const stepFields = activeStep.fields ?? [];
26
+ const isStepValid = stepFields.length === 0 || await trigger(stepFields, { shouldFocus: true });
27
+ if (!isStepValid) {
28
+ return;
29
+ }
30
+ setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));
31
+ setActiveIndex((currentIndex) => currentIndex + 1);
32
+ };
33
+ const skipStep = () => {
34
+ for (const field of activeStep.fields ?? []) {
35
+ resetField(field);
36
+ }
37
+ setSkippedKeys((currentKeys) => /* @__PURE__ */ new Set([...currentKeys, activeStep.key]));
38
+ setActiveIndex((currentIndex) => currentIndex + 1);
39
+ };
40
+ return /* @__PURE__ */ jsx(
41
+ WizardLayout,
42
+ {
43
+ steps,
44
+ activeIndex,
45
+ skippedKeys,
46
+ isSubmitting,
47
+ onBack: () => setActiveIndex((currentIndex) => currentIndex - 1),
48
+ onNext: goNext,
49
+ onSkip: skipStep,
50
+ submitButton: /* @__PURE__ */ jsx(SubmitButton, { children: submitLabel }),
51
+ problemAlert: /* @__PURE__ */ jsx(FormProblemAlert, {}),
52
+ texts
53
+ }
54
+ );
55
+ }
56
+ export {
57
+ FormWizard as default
58
+ };
59
+ //# sourceMappingURL=FormWizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormWizard.js","sources":["../../src/hookForm/FormWizard.jsx"],"sourcesContent":["// Formulario por pasos (wizard) con react-hook-form: el mismo del de formik (@stackcraftmx/calcita/forms), con las mismas\n// props y el mismo comportamiento:\n// - «Siguiente» revisa SOLO los campos del paso (trigger con sus nombres) y no avanza si alguno tiene error;\n// - «Omitir este paso» (solo en los opcionales) regresa sus campos a su valor de inicio (resetField): omitir es no dar ese\n// dato, no dejar lo que se escribió a medias;\n// - el último paso envía TODO el formulario (el esquema de yup completo vale para todos los pasos);\n// - si el envío falla por un campo de un paso anterior (yup o el 422 del servidor con applyServerProblem), regresa solo a\n// ese paso: un error que no se ve no se puede corregir.\n//\n// Va DENTRO de <FormProvider> y del <form> con su handleSubmit (usa useFormContext):\n// const form = useForm({resolver: yupResolver(schema), defaultValues});\n// <FormProvider {...form}>\n// <form noValidate onSubmit={form.handleSubmit(send)}>\n// <FormWizard submitLabel=\"Enviar solicitud\" steps={[\n// {key: 'datos', label: 'Sus datos', fields: ['name', 'email'], content: <>…campos…</>},\n// {key: 'aval', label: 'Aval', optional: true, fields: ['guarantor'], content: <>…</>},\n// ]} />\n// </form>\n// </FormProvider>\n//\n// Lo que se ve vive en WizardLayout (inputs/). Los textos se cambian con `texts`: {wizard: {next: 'Continuar'}}.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useFormContext} from 'react-hook-form';\n\nimport FormProblemAlert from '@/hookForm/FormProblemAlert.jsx';\nimport SubmitButton from '@/hookForm/SubmitButton.jsx';\nimport WizardLayout from '@/inputs/WizardLayout.jsx';\n\n/**\n * @param {object} props\n * @param {Object[]} props.steps en orden\n * @param {string} props.steps[].key\n * @param {string} props.steps[].label ya traducido\n * @param {string[]} [props.steps[].fields] los campos del paso (los que revisa «Siguiente»)\n * @param {boolean} [props.steps[].optional] se puede omitir\n * @param {import('react').ReactNode} props.steps[].content los campos del paso\n * @param {string} props.submitLabel ya traducido: el botón del último paso\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`: {wizard: {next, back, skip, optional, skipped, stepOf}}\n */\nexport default function FormWizard({steps, submitLabel, texts}) {\n const {trigger, resetField, getFieldState, formState} = useFormContext();\n const {isSubmitting, submitCount} = formState;\n const [activeIndex, setActiveIndex] = useState(0);\n const [skippedKeys, setSkippedKeys] = useState(() => new Set());\n const wasSubmitting = useRef(false);\n const activeStep = steps[activeIndex];\n\n // al terminar un envío fallido, regresa al paso con el primer error (solo en ese momento: escribir en otro paso no\n // debe jalar a la persona de regreso). OJO: getFieldState necesita formState para leer los errores de ahora\n useEffect(() => {\n if (wasSubmitting.current && !isSubmitting) {\n const errorStepIndex = steps.findIndex((step) => (step.fields ?? []).some((field) => getFieldState(field, formState).invalid));\n if (errorStepIndex >= 0) {\n setActiveIndex(errorStepIndex);\n }\n }\n wasSubmitting.current = isSubmitting;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isSubmitting, submitCount]);\n\n const goNext = async () => {\n const stepFields = activeStep.fields ?? [];\n // guard clause: el paso tiene errores; se queda para que se corrijan (trigger los marca y los pinta)\n const isStepValid = stepFields.length === 0 || await trigger(stepFields, {shouldFocus: true});\n if (!isStepValid) {\n return;\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys].filter((key) => key !== activeStep.key)));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n const skipStep = () => {\n for (const field of activeStep.fields ?? []) {\n resetField(field);\n }\n setSkippedKeys((currentKeys) => new Set([...currentKeys, activeStep.key]));\n setActiveIndex((currentIndex) => currentIndex + 1);\n };\n\n return (\n <WizardLayout steps={steps} activeIndex={activeIndex} skippedKeys={skippedKeys} isSubmitting={isSubmitting}\n onBack={() => setActiveIndex((currentIndex) => currentIndex - 1)} onNext={goNext} onSkip={skipStep}\n submitButton={<SubmitButton>{submitLabel}</SubmitButton>} problemAlert={<FormProblemAlert />} texts={texts} />\n );\n}\n"],"names":[],"mappings":";;;;;;;AAyCA,SAAwB,WAAW,EAAC,OAAO,aAAa,SAAQ;AAC5D,QAAM,EAAC,SAAS,YAAY,eAAe,UAAA,IAAa,eAAA;AACxD,QAAM,EAAC,cAAc,YAAA,IAAe;AACpC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,MAAM,oBAAI,KAAK;AAC9D,QAAM,gBAAgB,OAAO,KAAK;AAClC,QAAM,aAAa,MAAM,WAAW;AAIpC,YAAU,MAAM;AACZ,QAAI,cAAc,WAAW,CAAC,cAAc;AACxC,YAAM,iBAAiB,MAAM,UAAU,CAAC,UAAU,KAAK,UAAU,CAAA,GAAI,KAAK,CAAC,UAAU,cAAc,OAAO,SAAS,EAAE,OAAO,CAAC;AAC7H,UAAI,kBAAkB,GAAG;AACrB,uBAAe,cAAc;AAAA,MACjC;AAAA,IACJ;AACA,kBAAc,UAAU;AAAA,EAE5B,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,SAAS,YAAY;AACvB,UAAM,aAAa,WAAW,UAAU,CAAA;AAExC,UAAM,cAAc,WAAW,WAAW,KAAK,MAAM,QAAQ,YAAY,EAAC,aAAa,MAAK;AAC5F,QAAI,CAAC,aAAa;AACd;AAAA,IACJ;AACA,mBAAe,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,WAAW,EAAE,OAAO,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AACjG,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,QAAM,WAAW,MAAM;AACnB,eAAW,SAAS,WAAW,UAAU,CAAA,GAAI;AACzC,iBAAW,KAAK;AAAA,IACpB;AACA,mBAAe,CAAC,gBAAgB,oBAAI,IAAI,CAAC,GAAG,aAAa,WAAW,GAAG,CAAC,CAAC;AACzE,mBAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,EACrD;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAa;AAAA,MAAc;AAAA,MAA0B;AAAA,MAA0B;AAAA,MAClE,QAAQ,MAAM,eAAe,CAAC,iBAAiB,eAAe,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAQ,QAAQ;AAAA,MAC1F,cAAc,oBAAC,cAAA,EAAc,UAAA,YAAA,CAAY;AAAA,MAAiB,kCAAe,kBAAA,EAAiB;AAAA,MAAI;AAAA,IAAA;AAAA,EAAA;AAEpH;"}
@@ -7,6 +7,7 @@ import List from "@mui/material/List";
7
7
  import ListItemButton from "@mui/material/ListItemButton";
8
8
  import ListItemIcon from "@mui/material/ListItemIcon";
9
9
  import ListItemText from "@mui/material/ListItemText";
10
+ import ListSubheader from "@mui/material/ListSubheader";
10
11
  import Paper from "@mui/material/Paper";
11
12
  import Tab from "@mui/material/Tab";
12
13
  import Tabs from "@mui/material/Tabs";
@@ -18,8 +19,35 @@ const FOLDERS_WIDTH_PIXELS = 220;
18
19
  const LIST_WIDTH_PIXELS = 380;
19
20
  const DESKTOP_HEIGHT = "calc(100dvh - 220px)";
20
21
  const DESKTOP_MIN_HEIGHT_PIXELS = 480;
21
- function InboxLayout({ folders, selectedFolderId, onSelectFolder, toolbar, list, detail, isDetailOpen = false, onCloseDetail }) {
22
- const { t } = useToolkitTranslation("inbox");
22
+ const LABEL_DOT_PIXELS = 10;
23
+ function FolderRow({ folder, isSelected, onSelect, t }) {
24
+ const { id, label, Icon, color, unreadCount } = folder;
25
+ let icon = null;
26
+ if (Icon) {
27
+ icon = /* @__PURE__ */ jsx(Icon, { fontSize: "small" });
28
+ } else if (color) {
29
+ icon = /* @__PURE__ */ jsx(Box, { "aria-hidden": "true", sx: { width: LABEL_DOT_PIXELS, height: LABEL_DOT_PIXELS, borderRadius: "50%", bgcolor: `${color}.main`, ml: 0.5 } });
30
+ }
31
+ return /* @__PURE__ */ jsxs(ListItemButton, { selected: isSelected, onClick: () => onSelect(id), sx: { borderRadius: 2 }, children: [
32
+ icon && /* @__PURE__ */ jsx(ListItemIcon, { sx: { minWidth: 36 }, children: icon }),
33
+ /* @__PURE__ */ jsx(ListItemText, { primary: label }),
34
+ unreadCount > 0 && /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { fontWeight: 700 }, "aria-label": t("unreadCount", { count: unreadCount }), children: unreadCount })
35
+ ] });
36
+ }
37
+ function InboxLayout({
38
+ folders,
39
+ labels = [],
40
+ selectedFolderId,
41
+ onSelectFolder,
42
+ toolbar,
43
+ list,
44
+ detail,
45
+ isDetailOpen = false,
46
+ onCloseDetail,
47
+ foldersHeader,
48
+ texts
49
+ }) {
50
+ const { t } = useToolkitTranslation("inbox", texts);
23
51
  return /* @__PURE__ */ jsxs(Box, { sx: {
24
52
  display: "grid",
25
53
  gap: 2,
@@ -29,11 +57,19 @@ function InboxLayout({ folders, selectedFolderId, onSelectFolder, toolbar, list,
29
57
  minHeight: DESKTOP_MIN_HEIGHT_PIXELS
30
58
  }
31
59
  }, children: [
32
- /* @__PURE__ */ jsx(Paper, { variant: "outlined", component: "nav", "aria-label": t("folders"), sx: { p: 1, overflowY: "auto", borderRadius: 3, [PHONE_MEDIA_QUERY]: { display: "none" } }, children: /* @__PURE__ */ jsx(List, { disablePadding: true, children: folders.map(({ id, label, Icon, unreadCount }) => /* @__PURE__ */ jsxs(ListItemButton, { selected: id === selectedFolderId, onClick: () => onSelectFolder(id), sx: { borderRadius: 2 }, children: [
33
- Icon && /* @__PURE__ */ jsx(ListItemIcon, { sx: { minWidth: 36 }, children: /* @__PURE__ */ jsx(Icon, { fontSize: "small" }) }),
34
- /* @__PURE__ */ jsx(ListItemText, { primary: label }),
35
- unreadCount > 0 && /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { fontWeight: 700 }, "aria-label": t("unreadCount", { count: unreadCount }), children: unreadCount })
36
- ] }, id)) }) }),
60
+ /* @__PURE__ */ jsxs(Paper, { variant: "outlined", component: "nav", "aria-label": t("folders"), sx: { p: 1, overflowY: "auto", borderRadius: 3, [PHONE_MEDIA_QUERY]: { display: "none" } }, children: [
61
+ foldersHeader && /* @__PURE__ */ jsx(Box, { sx: { p: 0.5, pb: 1 }, children: foldersHeader }),
62
+ /* @__PURE__ */ jsx(List, { disablePadding: true, children: folders.map((folder) => /* @__PURE__ */ jsx(FolderRow, { folder, isSelected: folder.id === selectedFolderId, onSelect: onSelectFolder, t }, folder.id)) }),
63
+ labels.length > 0 && /* @__PURE__ */ jsx(
64
+ List,
65
+ {
66
+ disablePadding: true,
67
+ "aria-label": t("labels"),
68
+ subheader: /* @__PURE__ */ jsx(ListSubheader, { disableSticky: true, sx: { bgcolor: "transparent", lineHeight: 2.5, mt: 1 }, children: t("labels") }),
69
+ children: labels.map((label) => /* @__PURE__ */ jsx(FolderRow, { folder: label, isSelected: label.id === selectedFolderId, onSelect: onSelectFolder, t }, label.id))
70
+ }
71
+ )
72
+ ] }),
37
73
  /* @__PURE__ */ jsxs(Paper, { variant: "outlined", sx: {
38
74
  display: "flex",
39
75
  flexDirection: "column",
@@ -50,7 +86,7 @@ function InboxLayout({ folders, selectedFolderId, onSelectFolder, toolbar, list,
50
86
  variant: "scrollable",
51
87
  "aria-label": t("folders"),
52
88
  sx: { px: 1, [DESKTOP_MEDIA_QUERY]: { display: "none" } },
53
- children: folders.map(({ id, label, unreadCount }) => /* @__PURE__ */ jsx(Tab, { value: id, label: unreadCount > 0 ? /* @__PURE__ */ jsx(Badge, { color: "primary", badgeContent: unreadCount, sx: { pr: 1.5 }, children: label }) : label }, id))
89
+ children: [...folders, ...labels].map(({ id, label, unreadCount }) => /* @__PURE__ */ jsx(Tab, { value: id, label: unreadCount > 0 ? /* @__PURE__ */ jsx(Badge, { color: "primary", badgeContent: unreadCount, sx: { pr: 1.5 }, children: label }) : label }, id))
54
90
  }
55
91
  ),
56
92
  toolbar && /* @__PURE__ */ jsx(Box, { sx: { p: 1.5, pb: 1 }, children: toolbar }),