@stackcraftmx/calcita 0.32.1 → 0.47.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 (338) hide show
  1. package/README.md +263 -2
  2. package/cli/packageVersions.js +12 -3
  3. package/cli/projectFiles.js +5 -2
  4. package/cli/templates/module/__Name__Page.test.jsx.tpl +24 -0
  5. package/cli/templates/project/README.md.tpl +14 -1
  6. package/cli/templates/project/src/app/appRouter.jsx.tpl +5 -2
  7. package/cli/templates/project/src/core/accountConnector.js.tpl +4 -2
  8. package/cli/templates/project/src/modules/auth/SignInPage.test.jsx.tpl +20 -0
  9. package/cli/templates/project/src/modules/home/HomePage.test.jsx.tpl +16 -0
  10. package/cli/templates/project/src/testing/browserGaps.js.tpl +70 -0
  11. package/cli/templates/project/src/testing/openScreen.jsx.tpl +58 -0
  12. package/cli/templates/project/src/testing/setup.js.tpl +25 -0
  13. package/cli/templates/project/vite.config.js.tpl +12 -0
  14. package/dist/DataTable/GoToPage.js +69 -13
  15. package/dist/DataTable/GoToPage.js.map +1 -1
  16. package/dist/DataTable/ServerTable.js +2 -0
  17. package/dist/DataTable/ServerTable.js.map +1 -1
  18. package/dist/DataTable/ServerTableFilterDrawer.js +52 -0
  19. package/dist/DataTable/ServerTableFilterDrawer.js.map +1 -0
  20. package/dist/DataTable/filterContext.js.map +1 -1
  21. package/dist/DataTable/goToPageOptions.js +17 -0
  22. package/dist/DataTable/goToPageOptions.js.map +1 -0
  23. package/dist/date-pickers.js +28 -2
  24. package/dist/date-pickers.js.map +1 -1
  25. package/dist/datePickers/DateInput.js +15 -34
  26. package/dist/datePickers/DateInput.js.map +1 -1
  27. package/dist/datePickers/DateTimeInput.js +75 -0
  28. package/dist/datePickers/DateTimeInput.js.map +1 -0
  29. package/dist/datePickers/FormDateTimeField.js +12 -0
  30. package/dist/datePickers/FormDateTimeField.js.map +1 -0
  31. package/dist/datePickers/FormMonthField.js +12 -0
  32. package/dist/datePickers/FormMonthField.js.map +1 -0
  33. package/dist/datePickers/FormTimeField.js +12 -0
  34. package/dist/datePickers/FormTimeField.js.map +1 -0
  35. package/dist/datePickers/MonthInput.js +58 -0
  36. package/dist/datePickers/MonthInput.js.map +1 -0
  37. package/dist/datePickers/PickerFrame.js +27 -0
  38. package/dist/datePickers/PickerFrame.js.map +1 -0
  39. package/dist/datePickers/TimeInput.js +55 -0
  40. package/dist/datePickers/TimeInput.js.map +1 -0
  41. package/dist/datePickers/dateTimeRules.js +24 -0
  42. package/dist/datePickers/dateTimeRules.js.map +1 -0
  43. package/dist/datePickers/pickerValues.js +106 -0
  44. package/dist/datePickers/pickerValues.js.map +1 -0
  45. package/dist/datePickers/useCurrentMinute.js +28 -0
  46. package/dist/datePickers/useCurrentMinute.js.map +1 -0
  47. package/dist/datePickers/usePickerField.js +22 -0
  48. package/dist/datePickers/usePickerField.js.map +1 -0
  49. package/dist/dictionaries/dictionaries.js +3 -1
  50. package/dist/dictionaries/dictionaries.js.map +1 -1
  51. package/dist/forms/FormAttachmentsField.js +12 -0
  52. package/dist/forms/FormAttachmentsField.js.map +1 -0
  53. package/dist/forms/FormCheckboxGroupField.js +12 -0
  54. package/dist/forms/FormCheckboxGroupField.js.map +1 -0
  55. package/dist/forms/FormFileField.js +12 -0
  56. package/dist/forms/FormFileField.js.map +1 -0
  57. package/dist/forms/FormFileUploadField.js +12 -0
  58. package/dist/forms/FormFileUploadField.js.map +1 -0
  59. package/dist/forms/FormImageField.js +12 -0
  60. package/dist/forms/FormImageField.js.map +1 -0
  61. package/dist/forms/FormMaskedField.js +12 -0
  62. package/dist/forms/FormMaskedField.js.map +1 -0
  63. package/dist/forms/FormRepeatingFields.js +54 -0
  64. package/dist/forms/FormRepeatingFields.js.map +1 -0
  65. package/dist/forms/FormTransferListField.js +12 -0
  66. package/dist/forms/FormTransferListField.js.map +1 -0
  67. package/dist/forms/applyServerProblem.js +10 -3
  68. package/dist/forms/applyServerProblem.js.map +1 -1
  69. package/dist/forms/listRules.js +111 -0
  70. package/dist/forms/listRules.js.map +1 -0
  71. package/dist/forms/serverProblemFields.js +17 -0
  72. package/dist/forms/serverProblemFields.js.map +1 -1
  73. package/dist/forms.js +51 -29
  74. package/dist/forms.js.map +1 -1
  75. package/dist/hookForm/FormAttachmentsField.js +12 -0
  76. package/dist/hookForm/FormAttachmentsField.js.map +1 -0
  77. package/dist/hookForm/FormCheckboxGroupField.js +12 -0
  78. package/dist/hookForm/FormCheckboxGroupField.js.map +1 -0
  79. package/dist/hookForm/FormFileField.js +12 -0
  80. package/dist/hookForm/FormFileField.js.map +1 -0
  81. package/dist/hookForm/FormFileUploadField.js +12 -0
  82. package/dist/hookForm/FormFileUploadField.js.map +1 -0
  83. package/dist/hookForm/FormImageField.js +12 -0
  84. package/dist/hookForm/FormImageField.js.map +1 -0
  85. package/dist/hookForm/FormMaskedField.js +12 -0
  86. package/dist/hookForm/FormMaskedField.js.map +1 -0
  87. package/dist/hookForm/FormRepeatingFields.js +44 -0
  88. package/dist/hookForm/FormRepeatingFields.js.map +1 -0
  89. package/dist/hookForm/FormTransferListField.js +12 -0
  90. package/dist/hookForm/FormTransferListField.js.map +1 -0
  91. package/dist/hookForm/applyServerProblem.js +8 -3
  92. package/dist/hookForm/applyServerProblem.js.map +1 -1
  93. package/dist/index.js +188 -100
  94. package/dist/index.js.map +1 -1
  95. package/dist/inputs/CatalogInput.js +74 -7
  96. package/dist/inputs/CatalogInput.js.map +1 -1
  97. package/dist/inputs/CheckboxGroupInput.js +100 -0
  98. package/dist/inputs/CheckboxGroupInput.js.map +1 -0
  99. package/dist/inputs/MaskedInput.js +86 -0
  100. package/dist/inputs/MaskedInput.js.map +1 -0
  101. package/dist/inputs/NumberInput.js +12 -1
  102. package/dist/inputs/NumberInput.js.map +1 -1
  103. package/dist/inputs/RepeatingFields.js +177 -0
  104. package/dist/inputs/RepeatingFields.js.map +1 -0
  105. package/dist/inputs/SelectInput.js +43 -5
  106. package/dist/inputs/SelectInput.js.map +1 -1
  107. package/dist/inputs/TransferListInput.js +215 -0
  108. package/dist/inputs/TransferListInput.js.map +1 -0
  109. package/dist/inputs/inputMasks.js +84 -0
  110. package/dist/inputs/inputMasks.js.map +1 -0
  111. package/dist/inputs/selectAllState.js +20 -0
  112. package/dist/inputs/selectAllState.js.map +1 -0
  113. package/dist/inputs/useRowKeys.js +26 -0
  114. package/dist/inputs/useRowKeys.js.map +1 -0
  115. package/dist/legal/CookieNotice.js +79 -0
  116. package/dist/legal/CookieNotice.js.map +1 -0
  117. package/dist/legal/TermsAcceptanceDialog.js +129 -0
  118. package/dist/legal/TermsAcceptanceDialog.js.map +1 -0
  119. package/dist/legal/consentStorage.js +18 -0
  120. package/dist/legal/consentStorage.js.map +1 -0
  121. package/dist/legal/useCookieConsent.js +17 -0
  122. package/dist/legal/useCookieConsent.js.map +1 -0
  123. package/dist/locales/en.js +233 -1
  124. package/dist/locales/en.js.map +1 -1
  125. package/dist/locales/es.js +233 -1
  126. package/dist/locales/es.js.map +1 -1
  127. package/dist/mexico/CfdiSummary.js +81 -0
  128. package/dist/mexico/CfdiSummary.js.map +1 -0
  129. package/dist/mexico/EfirmaFields.js +167 -0
  130. package/dist/mexico/EfirmaFields.js.map +1 -0
  131. package/dist/mexico/certificateReader.js +79 -0
  132. package/dist/mexico/certificateReader.js.map +1 -0
  133. package/dist/mexico/cfdiReader.js +81 -0
  134. package/dist/mexico/cfdiReader.js.map +1 -0
  135. package/dist/mexico/derReader.js +81 -0
  136. package/dist/mexico/derReader.js.map +1 -0
  137. package/dist/mexico/efirmaKey.js +51 -0
  138. package/dist/mexico/efirmaKey.js.map +1 -0
  139. package/dist/mexico/mexicoIdentifiers.js +86 -0
  140. package/dist/mexico/mexicoIdentifiers.js.map +1 -0
  141. package/dist/mexico/mexicoRules.js +16 -0
  142. package/dist/mexico/mexicoRules.js.map +1 -0
  143. package/dist/mexico.js +32 -0
  144. package/dist/mexico.js.map +1 -0
  145. package/dist/react-hook-form.js +40 -17
  146. package/dist/react-hook-form.js.map +1 -1
  147. package/dist/rich-text.js +19 -0
  148. package/dist/rich-text.js.map +1 -0
  149. package/dist/richText/FormRichTextField.js +12 -0
  150. package/dist/richText/FormRichTextField.js.map +1 -0
  151. package/dist/richText/RichTextEditor.js +108 -0
  152. package/dist/richText/RichTextEditor.js.map +1 -0
  153. package/dist/richText/RichTextLinkPopover.js +57 -0
  154. package/dist/richText/RichTextLinkPopover.js.map +1 -0
  155. package/dist/richText/RichTextToolbar.js +189 -0
  156. package/dist/richText/RichTextToolbar.js.map +1 -0
  157. package/dist/richText/RichTextView.js +26 -0
  158. package/dist/richText/RichTextView.js.map +1 -0
  159. package/dist/richText/richTextContentSx.js +42 -0
  160. package/dist/richText/richTextContentSx.js.map +1 -0
  161. package/dist/richText/richTextExtensions.js +44 -0
  162. package/dist/richText/richTextExtensions.js.map +1 -0
  163. package/dist/richText/richTextRules.js +46 -0
  164. package/dist/richText/richTextRules.js.map +1 -0
  165. package/dist/security/SessionsCard.js +11 -7
  166. package/dist/security/SessionsCard.js.map +1 -1
  167. package/dist/table.js +2 -0
  168. package/dist/table.js.map +1 -1
  169. package/dist/theme/components/glassComponents.js +27 -0
  170. package/dist/theme/components/glassComponents.js.map +1 -1
  171. package/dist/theme/components/minimalComponents.js +12 -0
  172. package/dist/theme/components/minimalComponents.js.map +1 -1
  173. package/dist/types/DataTable/ServerTableFilterDrawer.d.ts +21 -0
  174. package/dist/types/DataTable/filterContext.d.ts +2 -1
  175. package/dist/types/DataTable/goToPageOptions.d.ts +15 -0
  176. package/dist/types/DataTable/index.d.ts +1 -0
  177. package/dist/types/date-pickers.d.ts +10 -1
  178. package/dist/types/datePickers/DateInput.d.ts +6 -4
  179. package/dist/types/datePickers/DateTimeInput.d.ts +50 -0
  180. package/dist/types/datePickers/FormDateTimeField.d.ts +7 -0
  181. package/dist/types/datePickers/FormMonthField.d.ts +7 -0
  182. package/dist/types/datePickers/FormTimeField.d.ts +7 -0
  183. package/dist/types/datePickers/MonthInput.d.ts +39 -0
  184. package/dist/types/datePickers/PickerFrame.d.ts +22 -0
  185. package/dist/types/datePickers/TimeInput.d.ts +38 -0
  186. package/dist/types/datePickers/dateTimeRules.d.ts +31 -0
  187. package/dist/types/datePickers/pickerValues.d.ts +114 -0
  188. package/dist/types/datePickers/useCurrentMinute.d.ts +5 -0
  189. package/dist/types/datePickers/usePickerField.d.ts +24 -0
  190. package/dist/types/dictionaries/dictionaries.d.ts +6 -3
  191. package/dist/types/forms/FormAttachmentsField.d.ts +7 -0
  192. package/dist/types/forms/FormCheckboxGroupField.d.ts +7 -0
  193. package/dist/types/forms/FormFileField.d.ts +7 -0
  194. package/dist/types/forms/FormFileUploadField.d.ts +7 -0
  195. package/dist/types/forms/FormImageField.d.ts +7 -0
  196. package/dist/types/forms/FormMaskedField.d.ts +7 -0
  197. package/dist/types/forms/FormRepeatingFields.d.ts +25 -0
  198. package/dist/types/forms/FormTransferListField.d.ts +7 -0
  199. package/dist/types/forms/index.d.ts +10 -1
  200. package/dist/types/forms/listRules.d.ts +48 -0
  201. package/dist/types/forms/serverProblemFields.d.ts +13 -0
  202. package/dist/types/forms.d.ts +1 -1
  203. package/dist/types/hookForm/FormAttachmentsField.d.ts +8 -0
  204. package/dist/types/hookForm/FormCheckboxGroupField.d.ts +8 -0
  205. package/dist/types/hookForm/FormFileField.d.ts +8 -0
  206. package/dist/types/hookForm/FormFileUploadField.d.ts +8 -0
  207. package/dist/types/hookForm/FormImageField.d.ts +8 -0
  208. package/dist/types/hookForm/FormMaskedField.d.ts +9 -0
  209. package/dist/types/hookForm/FormRepeatingFields.d.ts +27 -0
  210. package/dist/types/hookForm/FormTransferListField.d.ts +8 -0
  211. package/dist/types/index.d.ts +25 -3
  212. package/dist/types/inputs/CatalogInput.d.ts +7 -1
  213. package/dist/types/inputs/CheckboxGroupInput.d.ts +39 -0
  214. package/dist/types/inputs/MaskedInput.d.ts +35 -0
  215. package/dist/types/inputs/RepeatingFields.d.ts +52 -0
  216. package/dist/types/inputs/SelectInput.d.ts +6 -1
  217. package/dist/types/inputs/TransferListInput.d.ts +43 -0
  218. package/dist/types/inputs/inputMasks.d.ts +52 -0
  219. package/dist/types/inputs/selectAllState.d.ts +11 -0
  220. package/dist/types/inputs/useRowKeys.d.ts +11 -0
  221. package/dist/types/legal/CookieNotice.d.ts +30 -0
  222. package/dist/types/legal/TermsAcceptanceDialog.d.ts +32 -0
  223. package/dist/types/legal/consentStorage.d.ts +16 -0
  224. package/dist/types/legal/useCookieConsent.d.ts +13 -0
  225. package/dist/types/locales/en.d.ts +232 -0
  226. package/dist/types/locales/es.d.ts +232 -0
  227. package/dist/types/mexico/CfdiSummary.d.ts +17 -0
  228. package/dist/types/mexico/EfirmaFields.d.ts +38 -0
  229. package/dist/types/mexico/certificateReader.d.ts +37 -0
  230. package/dist/types/mexico/cfdiReader.d.ts +95 -0
  231. package/dist/types/mexico/derReader.d.ts +54 -0
  232. package/dist/types/mexico/efirmaKey.d.ts +26 -0
  233. package/dist/types/mexico/mexicoIdentifiers.d.ts +61 -0
  234. package/dist/types/mexico/mexicoRules.d.ts +29 -0
  235. package/dist/types/mexico.d.ts +11 -0
  236. package/dist/types/react-hook-form.d.ts +10 -1
  237. package/dist/types/rich-text.d.ts +5 -0
  238. package/dist/types/richText/FormRichTextField.d.ts +7 -0
  239. package/dist/types/richText/RichTextEditor.d.ts +42 -0
  240. package/dist/types/richText/RichTextLinkPopover.d.ts +17 -0
  241. package/dist/types/richText/RichTextToolbar.d.ts +15 -0
  242. package/dist/types/richText/RichTextView.d.ts +13 -0
  243. package/dist/types/richText/richTextContentSx.d.ts +7 -0
  244. package/dist/types/richText/richTextExtensions.d.ts +15 -0
  245. package/dist/types/richText/richTextRules.d.ts +41 -0
  246. package/dist/types/table.d.ts +1 -1
  247. package/dist/types/ui/actions/ActionsMenu.d.ts +18 -3
  248. package/dist/types/ui/actions/CopyButton.d.ts +7 -4
  249. package/dist/types/ui/actions/tableRowText.d.ts +6 -0
  250. package/dist/types/ui/cards/RecordCard.d.ts +65 -0
  251. package/dist/types/ui/cards/RecordCardSkeleton.d.ts +17 -0
  252. package/dist/types/ui/countdown/Countdown.d.ts +31 -0
  253. package/dist/types/ui/countdown/countdownParts.d.ts +39 -0
  254. package/dist/types/ui/countdown/useCountdown.d.ts +14 -0
  255. package/dist/types/ui/documents/PdfViewerDialog.d.ts +21 -0
  256. package/dist/types/ui/filters/FilterDrawer.d.ts +34 -0
  257. package/dist/types/ui/filters/filterDrawerFields.d.ts +139 -0
  258. package/dist/types/ui/help/HelpButton.d.ts +15 -0
  259. package/dist/types/ui/help/HelpCarouselDialog.d.ts +35 -0
  260. package/dist/types/ui/help/useHelpCarousel.d.ts +21 -0
  261. package/dist/types/ui/index.d.ts +10 -0
  262. package/dist/types/ui/labels/IconBadge.d.ts +34 -0
  263. package/dist/types/ui/navigation/RouteTabs.d.ts +44 -0
  264. package/dist/types/ui/navigation/routeTabMatching.d.ts +27 -0
  265. package/dist/types/upload/AttachmentsInput.d.ts +62 -0
  266. package/dist/types/upload/FileChip.d.ts +18 -0
  267. package/dist/types/upload/FileDropzone.d.ts +3 -0
  268. package/dist/types/upload/FileInput.d.ts +48 -0
  269. package/dist/types/upload/FileUploadInput.d.ts +80 -0
  270. package/dist/types/upload/ImageInput.d.ts +53 -0
  271. package/dist/types/upload/TemplateDownloadButton.d.ts +39 -0
  272. package/dist/types/upload/fileAppearance.d.ts +8 -0
  273. package/dist/types/upload/fileRules.d.ts +38 -0
  274. package/dist/types/upload/transferProgress.d.ts +18 -0
  275. package/dist/types/upload/uploadMachine.d.ts +6 -2
  276. package/dist/types/upload/uploadWithProgress.d.ts +6 -2
  277. package/dist/types/upload/useFilePicker.d.ts +39 -0
  278. package/dist/ui/actions/ActionsMenu.js +98 -15
  279. package/dist/ui/actions/ActionsMenu.js.map +1 -1
  280. package/dist/ui/actions/CopyButton.js +31 -11
  281. package/dist/ui/actions/CopyButton.js.map +1 -1
  282. package/dist/ui/actions/tableRowText.js +7 -0
  283. package/dist/ui/actions/tableRowText.js.map +1 -0
  284. package/dist/ui/cards/RecordCard.js +145 -0
  285. package/dist/ui/cards/RecordCard.js.map +1 -0
  286. package/dist/ui/cards/RecordCardSkeleton.js +33 -0
  287. package/dist/ui/cards/RecordCardSkeleton.js.map +1 -0
  288. package/dist/ui/countdown/Countdown.js +38 -0
  289. package/dist/ui/countdown/Countdown.js.map +1 -0
  290. package/dist/ui/countdown/countdownParts.js +34 -0
  291. package/dist/ui/countdown/countdownParts.js.map +1 -0
  292. package/dist/ui/countdown/useCountdown.js +35 -0
  293. package/dist/ui/countdown/useCountdown.js.map +1 -0
  294. package/dist/ui/documents/PdfViewerDialog.js +81 -0
  295. package/dist/ui/documents/PdfViewerDialog.js.map +1 -0
  296. package/dist/ui/filters/FilterDrawer.js +211 -0
  297. package/dist/ui/filters/FilterDrawer.js.map +1 -0
  298. package/dist/ui/filters/filterDrawerFields.js +119 -0
  299. package/dist/ui/filters/filterDrawerFields.js.map +1 -0
  300. package/dist/ui/help/HelpButton.js +17 -0
  301. package/dist/ui/help/HelpButton.js.map +1 -0
  302. package/dist/ui/help/HelpCarouselDialog.js +154 -0
  303. package/dist/ui/help/HelpCarouselDialog.js.map +1 -0
  304. package/dist/ui/help/useHelpCarousel.js +25 -0
  305. package/dist/ui/help/useHelpCarousel.js.map +1 -0
  306. package/dist/ui/labels/IconBadge.js +82 -0
  307. package/dist/ui/labels/IconBadge.js.map +1 -0
  308. package/dist/ui/navigation/RouteTabs.js +75 -0
  309. package/dist/ui/navigation/RouteTabs.js.map +1 -0
  310. package/dist/ui/navigation/routeTabMatching.js +23 -0
  311. package/dist/ui/navigation/routeTabMatching.js.map +1 -0
  312. package/dist/upload/AttachmentsInput.js +127 -0
  313. package/dist/upload/AttachmentsInput.js.map +1 -0
  314. package/dist/upload/FileChip.js +27 -0
  315. package/dist/upload/FileChip.js.map +1 -0
  316. package/dist/upload/FileDropzone.js +19 -84
  317. package/dist/upload/FileDropzone.js.map +1 -1
  318. package/dist/upload/FileInput.js +112 -0
  319. package/dist/upload/FileInput.js.map +1 -0
  320. package/dist/upload/FileUploadInput.js +285 -0
  321. package/dist/upload/FileUploadInput.js.map +1 -0
  322. package/dist/upload/ImageInput.js +125 -0
  323. package/dist/upload/ImageInput.js.map +1 -0
  324. package/dist/upload/TemplateDownloadButton.js +67 -0
  325. package/dist/upload/TemplateDownloadButton.js.map +1 -0
  326. package/dist/upload/fileAppearance.js +21 -0
  327. package/dist/upload/fileAppearance.js.map +1 -0
  328. package/dist/upload/fileRules.js +27 -1
  329. package/dist/upload/fileRules.js.map +1 -1
  330. package/dist/upload/transferProgress.js +23 -0
  331. package/dist/upload/transferProgress.js.map +1 -0
  332. package/dist/upload/uploadMachine.js +8 -8
  333. package/dist/upload/uploadMachine.js.map +1 -1
  334. package/dist/upload/uploadWithProgress.js +1 -1
  335. package/dist/upload/uploadWithProgress.js.map +1 -1
  336. package/dist/upload/useFilePicker.js +101 -0
  337. package/dist/upload/useFilePicker.js.map +1 -0
  338. package/package.json +44 -1
package/README.md CHANGED
@@ -49,6 +49,10 @@ pnpm exec calcita make:component OrderTotals --module purchase-orders
49
49
  El CLI nunca pisa un archivo que ya existe. Sus plantillas son los archivos tal cual (`cli/templates`), con
50
50
  `//#if pwa` … `//#endif` en lo que depende de una respuesta.
51
51
 
52
+ El proyecto trae pruebas de pantalla (`pnpm test`, Vitest con jsdom y Testing Library): cada una abre la app completa en una
53
+ dirección con el servidor de mentira (`openScreen` en `src/testing`) y pica como una persona. Vienen las del inicio, la de
54
+ entrar y las del módulo de ejemplo, y `make:module` arma la de cada módulo nuevo.
55
+
52
56
  ## Arrancar en un proyecto que ya existe
53
57
 
54
58
  ```bash
@@ -170,10 +174,10 @@ Cada puerta pide solo sus librerías; un proyecto instala lo que usa.
170
174
  | Importar de | Trae | Pide además |
171
175
  |---|---|---|
172
176
  | `@stackcraftmx/calcita` | proveedor, marco de la app, diálogos, avisos, descargas, encabezados, filtros, chips, métricas, formatos (`useFormatters`), fechas, errores RFC 9457, la petición de la casa (`createApiClient`), la recuperación de versión vieja, la navegación sin router (`createBrowserNavigation`, `createHashNavigation`), la búsqueda de la barra (`BarSearch`), el menú «⋯» (`ActionsMenu`), las acciones de lo elegido (`SelectionActions`), la seguridad de la cuenta (códigos de recuperación, sesiones, bitácora) y la bandeja tipo correo (`InboxLayout`, `MessageList` con selección, `MessageView` con la conversación) | — |
173
- | `@stackcraftmx/calcita/table` | `ServerTable`: paginada, filtrada y ordenada en el servidor, con su estado en la URL | `@mui/x-data-grid`, `nuqs` |
177
+ | `@stackcraftmx/calcita/table` | `ServerTable`: paginada, filtrada y ordenada en el servidor, con su estado en la URL; `ServerTableFilterDrawer`, su búsqueda en cajón | `@mui/x-data-grid`, `nuqs`, `react-window` |
174
178
  | `@stackcraftmx/calcita/forms` | campos de formik (texto, lista de una o varias opciones, número con miles, catálogo, color), formulario por pasos, aviso de arriba, errores del servidor pegados a cada campo | `formik`, `yup` |
175
179
  | `@stackcraftmx/calcita/react-hook-form` | los mismos campos con los mismos nombres, conectados a react-hook-form, su formulario por pasos y `yupResolver` | `react-hook-form`, `@hookform/resolvers` (y `yup` si valida con yup) |
176
- | `@stackcraftmx/calcita/date-pickers` | la fecha con el DatePicker de MUI X (`DateInput`, `FormDateField`); viaja `AAAA-MM-DD` | `@mui/x-date-pickers` 8, `dayjs` |
180
+ | `@stackcraftmx/calcita/date-pickers` | fecha, fecha con hora, hora y mes con los selectores de MUI X (`DateInput`, `DateTimeInput`, `TimeInput`, `MonthInput` y su versión de formik); cada uno viaja en texto | `@mui/x-date-pickers` 8, `dayjs` |
177
181
  | `@stackcraftmx/calcita/charts` | barras, columnas y líneas (una serie o varias), cada una con su vista de tabla, y `ChartTooltip` para las propias | `recharts` |
178
182
  | `@stackcraftmx/calcita/timeline` | línea de tiempo por grupos | `@mui/lab` |
179
183
  | `@stackcraftmx/calcita/excel` | reporte en Excel (Resumen y Datos) y CSV | `exceljs`, que se le pasa al llamarlo |
@@ -183,6 +187,8 @@ Cada puerta pide solo sus librerías; un proyecto instala lo que usa.
183
187
  | `@stackcraftmx/calcita/filepond` | `FileDropzone` con FilePond: el mismo uso que la de la puerta principal, más varios archivos, avance por archivo y `createPondServer` | `filepond`, `react-filepond`, `filepond-plugin-file-validate-type`, `filepond-plugin-file-validate-size` |
184
188
  | `@stackcraftmx/calcita/next` | la navegación de Next.js (App Router) para ToolkitProvider; los componentes ya vienen marcados con `'use client'` y los puros sirven en el servidor | `next` |
185
189
  | `@stackcraftmx/calcita/money` | `Money`: dinero exacto en centavos enteros, con suma, resta, multiplicación, división, potencia, raíz y su formato con la moneda | — |
190
+ | `@stackcraftmx/calcita/rich-text` | el editor de texto con formato (`RichTextEditor`, `FormRichTextField`) y la vista que enseña lo guardado sin dejar pasar código (`RichTextView`); viaja HTML | `@tiptap/react`, `@tiptap/pm`, `@tiptap/starter-kit`, `@tiptap/extensions`, `@tiptap/extension-text-align` (3) |
191
+ | `@stackcraftmx/calcita/mexico` | RFC, CURP y CLABE con su dígito verificador y sus reglas de yup; la e.firma (`EfirmaFields`: el .cer leído y la llave abierta en el navegador) y la factura (`readCfdi`, `CfdiSummary`) | `jsrsasign` |
186
192
  | `@stackcraftmx/calcita/passkeys` | llaves de acceso (passkeys): crearlas y firmar con ellas, con los errores del navegador en palabras de la persona | `@simplewebauthn/browser` 14 |
187
193
  | `@stackcraftmx/calcita/account` | la cuenta completa: `SignInScreen` (con el segundo paso), `ForgotPasswordScreen`, `ResetPasswordScreen`, `ProfileScreen` (con campos y pestañas propias), `AccountSecuritySection`, `AccountProvider` y `createRestAccountConnector` | `@simplewebauthn/browser` 14, `formik`, `yup` |
188
194
  | `@stackcraftmx/calcita/account-demo` | `createDemoAccountConnector`: la cuenta en el navegador, sin backend (y los códigos TOTP) | — |
@@ -245,6 +251,13 @@ el teléfono, con cápsulas, flotan arriba a la derecha y se quedan al bajar: la
245
251
  en un menú «⋯». Las `actions` de `PageHeading` ya suben solas, y `ServerTable` sube su búsqueda con
246
252
  `searchInBar`. «Hecho en Calcita» → «Marco y menú» lo enseña vivo.
247
253
 
254
+ Cuando los filtros de una lista ya no caben arriba de la tabla, la búsqueda en cajón: `FilterDrawer` sale de un lado
255
+ (`anchor`) con los campos declarados una vez (`FILTER_FIELD_KIND`: texto, una o varias opciones, catálogo, casillas, rango de
256
+ números, de fechas, monto con operador o uno propio); lo que se cambia adentro no mueve la lista hasta «Buscar». Con
257
+ `ServerTableFilterDrawer` en `toolbarExtra` va conectado a la tabla: botón «Filtros (N)», filtros en la dirección y un chip
258
+ por filtro (`filterDrawerDefaultsOf`, `filterDrawerKindsOf`, `filterDrawerChipsOf`). Un monto viaja como `gte:500`
259
+ (`comparisonOf` lo separa en el backend o en la pantalla).
260
+
248
261
  ```jsx
249
262
  <BarActions label="Buscar en pedidos" order={1}>
250
263
  <IconButton aria-label="Buscar" onClick={openSearch}><SearchRoundedIcon /></IconButton>
@@ -308,6 +321,7 @@ dejar Calcita con su propia cara, parte por parte.
308
321
  | `CalcitaActivityLog` | `itemIcon` |
309
322
  | `CalcitaAppearanceSettings` | `selectSection`, `section`, `sectionLabel`, `option`, `divider` |
310
323
  | `CalcitaAppLayout` | `skipLink`, `brand`, `brandLogo`, `brandText`, `brandName`, `brandCaption`, `titleArea`, `phoneTitle`, `desktopTitle`, `railBrand`, `railLogo`, `collapseButton`, `root`, `scrollEdge`, `bar`, `toolbar`, `menuButton`, `spacer`, `tools`, `progress`, `body`, `drawer`, `main`, `phoneSlot`, `content`, `phoneDrawer` |
324
+ | `CalcitaAttachmentsInput` | `root`, `actions`, `button`, `list`, `fileChip`, `limits` |
311
325
  | `CalcitaAuditTrailDialog` | `description` |
312
326
  | `CalcitaAuthLayout` | `root`, `tools`, `body`, `brand`, `logo`, `brandName`, `card`, `description`, `content`, `footer` |
313
327
  | `CalcitaBarActions` | `group` |
@@ -315,14 +329,18 @@ dejar Calcita con su propia cara, parte por parte.
315
329
  | `CalcitaBarList` | `row`, `chart` |
316
330
  | `CalcitaBarSearch` | `field` |
317
331
  | `CalcitaCalendarHeatmap` | `header`, `toggle`, `tableArea`, `dateCell`, `valueCell`, `descriptionCell`, `gridArea`, `grid`, `monthLabel`, `week`, `dayLabel`, `cell`, `legend`, `legendLess`, `legendCell`, `legendMore` |
332
+ | `CalcitaCfdiSummary` | `root`, `header`, `amounts`, `concepts` |
318
333
  | `CalcitaChart` | `tableArea`, `legend`, `legendItem`, `legendLine`, `legendSwatch`, `reference`, `referenceLine` |
319
334
  | `CalcitaChartTooltip` | `root` |
335
+ | `CalcitaCheckboxGroupInput` | `root`, `selectAll`, `option` |
320
336
  | `CalcitaCollapsibleSection` | `root`, `accordion`, `summary`, `titleArea`, `collapsedTitle`, `expandedTitle`, `details` |
321
337
  | `CalcitaColorInput` | `swatch`, `nativeInput` |
322
338
  | `CalcitaColorSchemeToggle` | `button` |
323
339
  | `CalcitaColumnChart` | `chart` |
324
340
  | `CalcitaConfirmDialog` | `problem`, `actions` |
325
341
  | `CalcitaContentDialog` | `header`, `title`, `closeButton`, `body` |
342
+ | `CalcitaCookieNotice` | `root`, `text`, `actions` |
343
+ | `CalcitaCountdown` | `root` |
326
344
  | `CalcitaDateRangeFilter` | `chip`, `panel`, `actions` |
327
345
  | `CalcitaDefinitionList` | `root`, `entry`, `label`, `value` |
328
346
  | `CalcitaDemoTotpCode` | `root`, `code` |
@@ -330,11 +348,15 @@ dejar Calcita con su propia cara, parte por parte.
330
348
  | `CalcitaDownloadManager` | `doneIcon`, `failedIcon`, `canceledIcon`, `cancelButton`, `progress`, `cancelIcon`, `item`, `itemText`, `itemStatus`, `itemActions`, `panel`, `header`, `title`, `list` |
331
349
  | `CalcitaDownloadMenu` | `button` |
332
350
  | `CalcitaEmptyState` | `root`, `icon` |
351
+ | `CalcitaEfirmaFields` | `root`, `certificate`, `verify` |
333
352
  | `CalcitaErrorPage` | `root`, `actions` |
334
353
  | `CalcitaEventCalendar` | `toolbar`, `navigation`, `title`, `calendar` |
335
354
  | `CalcitaEventDetails` | `row`, `rowContent`, `titleRow`, `toneDot`, `linkRow`, `link`, `guestsTitle`, `guest`, `guestAvatar`, `guestName`, `notes`, `actions` |
336
355
  | `CalcitaFileDropzone` | `root`, `fileArea`, `fileChip`, `hint`, `actions`, `limits`, `extra` |
356
+ | `CalcitaFileInput` | `root`, `button`, `fileChip`, `empty` |
357
+ | `CalcitaFileUploadInput` | `root`, `fileName`, `status`, `button`, `helper` |
337
358
  | `CalcitaFilterChips` | `root`, `header`, `clearButton` |
359
+ | `CalcitaFilterDrawer` | `header`, `body`, `footer` (el panel: clase `CalcitaFilterDrawer-paper`) |
338
360
  | `CalcitaFontSizeToggle` | `button` |
339
361
  | `CalcitaFormProblemAlert` | `root` |
340
362
  | `CalcitaFormRow` | `root`, `label`, `control` |
@@ -345,6 +367,9 @@ dejar Calcita con su propia cara, parte por parte.
345
367
  | `CalcitaGroupedTimeline` | `group`, `groupHeader`, `groupButton`, `groupTitle`, `expandIcon`, `timeline`, `opposite`, `connector`, `content`, `entryTitle` |
346
368
  | `CalcitaHeaderFilter` | `button`, `badge`, `panel` |
347
369
  | `CalcitaHeaderTools` | `divider`, `root` |
370
+ | `CalcitaHelpCarouselDialog` | `media`, `content`, `dots`, `actions` |
371
+ | `CalcitaIconBadge` | `root`, `icon`, `label` |
372
+ | `CalcitaImageInput` | `root`, `preview`, `image`, `placeholder`, `actions` |
348
373
  | `CalcitaInboxLayout` | `labelDot`, `folder`, `folderIcon`, `folderCount`, `root`, `folders`, `foldersHeader`, `labelsTitle`, `listPane`, `phoneTabs`, `phoneTabBadge`, `listHeader`, `list`, `detail`, `backButton` |
349
374
  | `CalcitaInstallAppSetting` | `root` |
350
375
  | `CalcitaInstallInstructions` | `steps` |
@@ -370,8 +395,14 @@ dejar Calcita con su propia cara, parte por parte.
370
395
  | `CalcitaProfileHeader` | `root`, `cover`, `body`, `avatar`, `text`, `name`, `chips`, `actions` |
371
396
  | `CalcitaProfileScreen` | `sectionTitle`, `form`, `preferences` |
372
397
  | `CalcitaQuickFilters` | `root`, `caption`, `chip`, `chipLabel`, `chipCount` |
398
+ | `CalcitaPdfViewerDialog` | `actions`, `frame` |
399
+ | `CalcitaRecordCard` | `root`, `header`, `avatar`, `titles`, `status`, `body`, `group`, `fact`, `highlight`, `footer`, `details` |
373
400
  | `CalcitaRecoveryCodesDialog` | `codes`, `actions` |
401
+ | `CalcitaRouteTabs` | `root`, `tab`, `badge` |
374
402
  | `CalcitaScrollableRow` | `root` |
403
+ | `CalcitaRepeatingFields` | `root`, `row`, `rowHeader`, `rowContent`, `addButton`, `helper` |
404
+ | `CalcitaRichTextEditor` | `root`, `toolbar`, `content`, `footer` |
405
+ | `CalcitaRichTextView` | `root` |
375
406
  | `CalcitaSecondStepForm` | `actions` |
376
407
  | `CalcitaSectionCard` | `root`, `header`, `title` |
377
408
  | `CalcitaSegmentedControl` | `root` |
@@ -390,8 +421,11 @@ dejar Calcita con su propia cara, parte por parte.
390
421
  | `CalcitaTabBarEditor` | `root`, `header`, `title`, `list`, `itemIcon`, `footer` |
391
422
  | `CalcitaTableToggle` | `root`, `button` |
392
423
  | `CalcitaTag` | `root` |
424
+ | `CalcitaTemplateDownloadButton` | `root` |
425
+ | `CalcitaTermsAcceptanceDialog` | `content`, `frame`, `footer` |
393
426
  | `CalcitaTextInput` | `field` |
394
427
  | `CalcitaTopMenu` | `link`, `root` |
428
+ | `CalcitaTransferListInput` | `root`, `list`, `actions` |
395
429
  | `CalcitaTwoStepVerification` | `qr`, `secret`, `secretCode`, `header`, `headerTitle`, `headerText`, `menuIcon` |
396
430
  | `CalcitaWizardLayout` | `stepper`, `phoneSteps`, `actions` |
397
431
  | `CalcitaWordmark` | `root` |
@@ -488,6 +522,25 @@ const menuItems = composeMenu(import.meta.glob('@/modules/*/menu.js', {eager: tr
488
522
 
489
523
  En cada pantalla, `PageHeading` pone el título, la descripción y los botones, y arma las migas solo desde el menú
490
524
  (Inicio › Sección › Pantalla). `PageHeader` es el encabezado centrado de una portada y `DetailHeader` el de un registro.
525
+ Para explicar una pantalla la primera vez que se entra, la ayuda en carrusel: `HelpCarouselDialog` (pasos con imagen o
526
+ ilustración, título y texto; Siguiente, los puntos, las flechas o deslizando) y `useHelpCarousel({helpKey, version})`, que
527
+ la abre sola la primera vez y por versión, hasta que se termina o se marca «No volver a mostrar» (`seen` para lo que ya sabe
528
+ el servidor del proyecto); `HelpButton` es el «?» que la vuelve a abrir.
529
+
530
+ Para listas donde una tabla se queda corta, `RecordCard`: arriba quién es (avatar con iniciales, folio, nombre) con su
531
+ estatus, insignias, botón para abrirlo y menú «⋯»; en el cuerpo sus datos en grupos (`groups`, o `content` libre) y el dato
532
+ principal grande; abajo lo que va a todo lo ancho y «Ver detalle», que despliega más. El orden es el que se da y nada se
533
+ reacomoda solo: para un orden obligatorio, `trailing` pone el estatus al final y `columns` fija el ancho de cada columna.
534
+ `RecordCardSkeleton` es su esqueleto. El menú «⋯» (`ActionsMenu`) acepta secciones, separadores, ligas, renglones propios y
535
+ submenús (`children`), que se abren dentro del mismo menú con «Regresar».
536
+
537
+ Para las secciones de un registro, `RouteTabs`: pestañas donde cada una es una dirección (se comparte la liga a una pestaña y
538
+ «atrás» regresa a la anterior); por ruta (cada pestaña su página) o en la consulta (`mode="query"`, `?tab=pagos`, con
539
+ `useActiveRouteTab` para pintar el panel), con ícono, conteo y permiso por pestaña. Junto al título de un registro van su
540
+ estatus (`StatusChip`) y sus insignias (`IconBadge`): un ícono y un texto corto con el
541
+ color de lo que significan («Envío asegurado», «Pago con tarjeta»), con borde, tenue o sin caja. Mejor desde un diccionario:
542
+ la entrada trae su ícono (`dictionaryEntryOf(value, keyPrefix, color, {icon})`) y la insignia saca de ahí texto,
543
+ explicación, color e ícono (`<IconBadge dictionary={PAYMENT_METHOD} value={order.payment_method} />`).
491
544
 
492
545
  La misma regla protege las rutas (`withAccess(access, routes)` de `/react-router`: quien no puede ve «sin permiso», o la
493
546
  página propia que se pase en `{fallback}`) y
@@ -819,6 +872,108 @@ import {FormRow, FormSection} from '@stackcraftmx/calcita';
819
872
  Los campos van directo dentro de la `FormSection` (sin Grid ni Stack en medio): la línea entre renglones se dibuja entre
820
873
  hermanos.
821
874
 
875
+ ### Campos con máscara
876
+
877
+ `MaskedInput` (y `FormMaskedField` en `/forms` y `/react-hook-form`) es el campo de texto de la casa con una máscara: mientras
878
+ se escribe se ven los separadores («555 123 4567») y al formulario llega el valor limpio («5551234567»). Pegar un número con
879
+ otros separadores también sirve. Las de siempre vienen con nombre en `INPUT_MASK` (`phone`, `postalCode`, `clabe`, `card`,
880
+ `rfc`, `curp`, `uuid`); una propia es un patrón donde `9` es un dígito, `A` una letra, `X` letra o dígito y `H` hexadecimal
881
+ (`mask="AAA-9999"`). El RFC y la CURP solo se filtran y se pasan a mayúsculas mientras se escriben.
882
+
883
+ ```jsx
884
+ <FormMaskedField name="phone" mask={INPUT_MASK.phone} label="Teléfono" placeholder="555 123 4567" />
885
+ <FormMaskedField name="clabe" mask="clabe" label="CLABE" />
886
+ ```
887
+
888
+ ### Fechas y horas
889
+
890
+ Cuatro campos en `/date-pickers`, con los selectores de MUI X. Cada uno viaja en texto con una sola forma y se ve en el
891
+ idioma de la persona; la pantalla no convierte nada:
892
+
893
+ | Campo (suelto · formik) | Para qué | Viaja |
894
+ |---|---|---|
895
+ | `DateInput` · `FormDateField` | una fecha de calendario (un nacimiento, una entrega) | `'2026-11-01'` |
896
+ | `DateTimeInput` · `FormDateTimeField` | un momento que pasa una vez (un envío programado, una cita) | `'2026-11-01T18:30:00Z'`, en UTC |
897
+ | `TimeInput` · `FormTimeField` | una hora del día sin fecha (un horario) | `'09:30'`, de 24 horas |
898
+ | `MonthInput` · `FormMonthField` | el mes de un reporte o de un cierre | `'2026-11'` |
899
+
900
+ La fecha con hora se ve en la zona del proyecto (`ToolkitProvider timeZone`) con el nombre de la zona abajo («Hora estándar
901
+ central»), para que nadie dude en qué hora está escribiendo; `timeZone` la cambia en un solo campo (la hora de una
902
+ sucursal) y el momento que viaja es el mismo. La hora sale de 24 horas o con a. m. y p. m. según `ToolkitProvider
903
+ hourCycle`, y viaja igual. Lo que el selector de MUI X acepta y aquí no tiene nombre va en `pickerProps`.
904
+
905
+ ```jsx
906
+ import {FormDateTimeField, FormMonthField, FormTimeField} from '@stackcraftmx/calcita/date-pickers';
907
+
908
+ <FormDateTimeField name="visitAt" label="Fecha y hora" disablePast />
909
+ <FormTimeField name="opensAt" label="Abre a las" minTime="07:00" maxTime="21:00" />
910
+ <FormMonthField name="billingMonth" label="Se cobra en" disableFuture />
911
+ ```
912
+
913
+ Con react-hook-form, el campo suelto va dentro de un `Controller` (la puerta de fechas no pide react-hook-form).
914
+
915
+ Los minutos van de uno en uno (MUI X trae de 5 en 5); `minutesStep` los cambia. Para algo que se publica con
916
+ anticipación y dura un mínimo (una promoción, una venta especial), el inicio lleva `minimumMinutesFromNow` (el mínimo se recorre
917
+ solo con el reloj mientras la pantalla está abierta) y el fin `minDateTime={instantPlusMinutes(startsAt, 30)}`; en yup,
918
+ las mismas reglas con su mensaje. El servidor vuelve a revisar: entre llenar el formulario y guardarlo pasa el tiempo.
919
+
920
+ ```jsx
921
+ import {FormDateTimeField, instantPlusMinutes, minutesAfterRule, minutesFromNowRule} from '@stackcraftmx/calcita/date-pickers';
922
+
923
+ const schema = Yup.object({
924
+ startsAt: Yup.string().required().test(minutesFromNowRule(5)),
925
+ endsAt: Yup.string().required().test(minutesAfterRule('startsAt', 30)),
926
+ });
927
+
928
+ <FormDateTimeField name="startsAt" label="Empieza" minimumMinutesFromNow={5} />
929
+ <FormDateTimeField name="endsAt" label="Termina" minDateTime={instantPlusMinutes(values.startsAt, 30)} />
930
+ ```
931
+
932
+ ### Renglones que se repiten
933
+
934
+ `FormRepeatingFields` (de formik y de react-hook-form, con el mismo uso) pinta una lista del formulario: cada renglón con
935
+ su número y «Quitar», y «Agregar» al final. `minRows` (por omisión 1: con el mínimo, «Quitar» limpia el renglón) y
936
+ `maxRows`; si un renglón se puede quitar lo decide la pantalla (`removeBlockedReason` da el motivo, `confirmRemove` pide
937
+ confirmar uno guardado). En escritorio los campos de un renglón van lado a lado (`columns`) y en el teléfono uno debajo de
938
+ otro. Las reglas de listas marcan el error en SU renglón:
939
+
940
+ ```jsx
941
+ import {FormNumberField, FormRepeatingFields, Yup, listTotalAtMostRule, orderedInListRule, uniqueInListRule} from '@stackcraftmx/calcita/forms';
942
+
943
+ const schema = Yup.object({
944
+ prices: Yup.array().of(Yup.object({
945
+ min_quantity: Yup.string().required()
946
+ .test(uniqueInListRule('prices', 'min_quantity')) // «Ya está en el renglón 2»
947
+ .test(orderedInListRule('prices', 'min_quantity')), // «Debe ir entre 10 (renglón 1) y 50 (renglón 3)»
948
+ })),
949
+ split: Yup.array().test(listTotalAtMostRule('amount', (values) => values.credit_limit)), // con Money, sin floats
950
+ });
951
+
952
+ <FormRepeatingFields name="prices" label="Precios por volumen" emptyRow={{min_quantity: '', unit_price: ''}} maxRows={6}
953
+ renderRow={({nameOf}) => (<>
954
+ <FormNumberField name={nameOf('min_quantity')} label="Desde (piezas)" decimals={0} />
955
+ <FormNumberField name={nameOf('unit_price')} label="Precio" prefix="$" />
956
+ </>)} />
957
+ ```
958
+
959
+ ### Elegir varios
960
+
961
+ Tres formas, según cuántas opciones hay, y cada una con su versión de formik y de react-hook-form:
962
+
963
+ | Pieza | Para qué | Opciones de varias |
964
+ |---|---|---|
965
+ | `CheckboxGroupInput` · `FormCheckboxGroupField` | pocas opciones a la vista (los canales de aviso) | `selectAll`, `row`, `options[].description` |
966
+ | `SelectInput` · `FormSelectField` con `multiple` | una lista corta (los días) | `selectAll`, `checkboxes` |
967
+ | `CatalogInput` · `FormCatalogField` con `multiple` | un catálogo largo con buscador (los estados) | `selectAll` (con algo buscado, «Elegir los N que coinciden»), `checkboxes`, `limitTags` |
968
+ | `TransferListInput` · `FormTransferListField` | elegir muchos viendo qué quedó de cada lado (compradores, sucursales) | `availableTitle`, `chosenTitle`, `searchable`, `listHeight` |
969
+
970
+ «Elegir todos» lleva su casilla a medias cuando hay algunos elegidos y no cuenta las opciones apagadas (`disabled`). La
971
+ lista de transferencia va fuera de una FormSection: necesita el ancho completo.
972
+
973
+ Un 422 del servidor en `contacts.2.email` llega a ese renglón (`applyServerProblem`). Con formik, el error de toda la lista
974
+ (el tope) sale cuando los renglones ya no tienen errores: formik guarda los dos en el mismo lugar; para verlo mientras se
975
+ escribe va un resumen en vivo en `footer` con `listTotalOf`.
976
+
822
977
  ## Subir archivos
823
978
 
824
979
  Dos zonas para soltar archivos con el mismo uso: la de la puerta principal, con puro MUI (un archivo), y la de
@@ -835,6 +990,112 @@ import {FileDropzone, buildUploadActors, uploadMachine, uploadWithProgress} from
835
990
  `accept` acepta nombres (`'excel'`, `['pdf', 'image']`) o lo de siempre del input (`'.xlsx,.xls'`); `maxSize` va en bytes
836
991
  o con unidades (`'10 MB'`, en base 1000 como el navegador).
837
992
 
993
+ Para otros acomodos, con las mismas reglas y los mismos avisos:
994
+
995
+ | Pieza (suelta · formik y react-hook-form) | Para qué | Al formulario llega |
996
+ |---|---|---|
997
+ | `FileInput` · `FormFileField` | un archivo del tamaño de un campo (la constancia, el contrato) | el `File`, lo ya guardado (`{name, size}`) o `null` |
998
+ | `AttachmentsInput` · `FormAttachmentsField` | varios, como en un correo: «Adjuntar» y un chip por archivo | la lista (nuevos y guardados) |
999
+ | `ImageInput` · `FormImageField` | una foto con su vista previa, en círculo o recuadro | el `File`, la dirección de la guardada o `null` |
1000
+ | `TemplateDownloadButton` | «Descargar la plantilla» (un archivo fijo con `href` o su API con `request`) | — |
1001
+
1002
+ Los cuatro cargadores aceptan `template` para poner «Descargar la plantilla» junto a su botón, y `AttachmentsInput` suma
1003
+ `maxFiles` y `maxTotalSize`. Para armar uno propio (una tarjeta, un botón en la barra) está `useFilePicker`, que da el
1004
+ input, abrir la ventana, soltar encima y la revisión con sus avisos.
1005
+
1006
+ ```jsx
1007
+ import {AttachmentsInput, FileInput} from '@stackcraftmx/calcita';
1008
+
1009
+ <FileInput label="Constancia fiscal" value={file} onChange={setFile} accept="pdf" maxSize="2 MB"
1010
+ template={{href: '/plantillas/constancia.pdf'}} />
1011
+ <AttachmentsInput value={files} onChange={setFiles} accept={['pdf', 'image']} maxFiles={5} maxTotalSize="10 MB" />
1012
+ ```
1013
+
1014
+ ### El cargador mini: sube en cuanto se elige
1015
+
1016
+ Los de arriba solo eligen y el archivo viaja con el formulario. `FileUploadInput` (y `FormFileUploadField` de formik y de
1017
+ react-hook-form) SUBE el archivo en cuanto se elige, en su renglón: para una lista de documentos (lo que pide una solicitud,
1018
+ el expediente de un cliente). Enseña su estado (Por cargar, Subiendo 40 %, Procesando, Cargado, No se cargó), «Cargar» que ya
1019
+ cargado dice «Sustituir», bajar el subido y quitarlo, y abajo el avance en palabras o el motivo del error. Al formulario
1020
+ llega solo el id que da el servidor (`''` mientras sube): `Yup.string().required()` pide el documento.
1021
+
1022
+ ```jsx
1023
+ import {FormFileUploadField} from '@stackcraftmx/calcita/forms';
1024
+
1025
+ <FormFileUploadField name="identification_file_id" label="Identificación oficial" accept={['pdf', 'image']} maxSize="5 MB"
1026
+ upload={(file, {signal, onUploadProgress, idempotencyKey}) => documentsApi.upload(file, {signal, onUploadProgress, idempotencyKey})}
1027
+ download={(fileId, transferOptions) => documentsApi.download(fileId, transferOptions)} />
1028
+ ```
1029
+
1030
+ `upload` es la subida del proyecto (`uploadWithProgress`, axios o su cliente) y `idOf` dice qué guardar de la respuesta (por
1031
+ omisión `result.id`). Lo del negocio (Aprobado, Falta información) va junto al estado con `actions`, y `onStatusChange`
1032
+ dice en qué va cada renglón para contar «2 de 3 cargados».
1033
+
1034
+ ## Documentos, avisos legales y cuenta regresiva
1035
+
1036
+ - `PdfViewerDialog`: un PDF en una ventana con el visor del navegador (sin librerías), con «Abrir en otra pestaña» y
1037
+ «Descargar» siempre a la vista (en el iPhone el visor dentro de la página enseña solo la primera hoja). `src` es la
1038
+ dirección o el archivo (un Blob de su API).
1039
+ - `CookieNotice`: el aviso de cookies abajo, hasta que se acepta ESA versión en el navegador; no bloquea la pantalla.
1040
+ `useCookieConsent` para pintar uno propio.
1041
+ - `TermsAcceptanceDialog`: aceptar los términos antes de seguir; pide leerlos hasta el final, marcar la casilla y «Acepto»,
1042
+ y no se cierra con Escape. Quién aceptó qué versión lo registra su API en `onAccept` (la ventana espera y, si falla, dice
1043
+ el motivo).
1044
+ - `Countdown`: «02:14:30» hasta un momento, con días cuando faltan, lo que dice al llegar a cero y `onComplete`;
1045
+ `useCountdown` da las partes para pintarla a su manera.
1046
+
1047
+ ```jsx
1048
+ <CookieNotice version="2026-11" policyHref="/legal/cookies" />
1049
+ <TermsAcceptanceDialog open={!session.terms.accepted} version={session.terms.current} onAccept={({version}) => api.acceptTerms(version)}
1050
+ onDecline={signOut}>
1051
+ <TermsText />
1052
+ </TermsAcceptanceDialog>
1053
+ <Countdown until={registration.closes_at} onComplete={reloadRegistration} completedText="Cerradas" />
1054
+ ```
1055
+
1056
+ ## Texto con formato
1057
+
1058
+ `@stackcraftmx/calcita/rich-text` trae el editor con Tiptap: estilo (texto, título, subtítulo), negrita, cursiva,
1059
+ subrayado, tachado, listas, cita, código, ligas, alineación, quitar el formato y deshacer, con contador si hay `maxLength`.
1060
+ Viaja HTML; vacío es `''` (así `required()` funciona). Las ligas solo con https, http, mailto o tel (un dominio suelto se
1061
+ completa con https://). `RichTextView` enseña lo guardado pasándolo por el mismo esquema del editor: un `<script>`, un
1062
+ `onclick` o una liga `javascript:` no llegan a la página.
1063
+
1064
+ ```jsx
1065
+ import {FormRichTextField, RICH_TEXT_FEATURE, RichTextView} from '@stackcraftmx/calcita/rich-text';
1066
+
1067
+ <FormRichTextField name="notes" label="Notas" maxLength={2000}
1068
+ features={[RICH_TEXT_FEATURE.bold, RICH_TEXT_FEATURE.bulletList, RICH_TEXT_FEATURE.link]} />
1069
+ <RichTextView value={order.notes} />
1070
+ ```
1071
+
1072
+ Para lo que la barra no trae: `toolbarActions` (botones propios), `onEditorReady` (el editor de Tiptap) y `extensions`
1073
+ (extensiones de Tiptap del proyecto). Pesa unos 420 kB (134 kB comprimido) y solo se baja en las pantallas que lo usan.
1074
+
1075
+ ## México: RFC, CURP, CLABE, e.firma y facturas
1076
+
1077
+ `@stackcraftmx/calcita/mexico` trae lo que casi cualquier sistema en México pide (pide `jsrsasign` solo para abrir la llave):
1078
+
1079
+ - `isValidRfc`, `isValidCurp`, `isValidClabe`: la forma, que la fecha exista y el dígito verificador (el del RFC, con
1080
+ `{strict: true}`). En yup, como reglas con su mensaje: `Yup.string().test(clabeRule())`.
1081
+ - `readCfdi(xmlText)` / `readCfdiFile(file)`: la factura (CFDI 3.3 y 4.0) leída del XML, con los montos en texto para
1082
+ `Money`; `null` si no es factura. `CfdiSummary` la enseña, con el aviso si no es para su RFC o si no está timbrada.
1083
+ - `EfirmaFields`: el .cer, el .key y la contraseña. El certificado se lee en el navegador (`readCertificate`: RFC, CURP, nombre,
1084
+ número de certificado, vigencia) y avisa si venció o es de otro RFC. «Revisar la e.firma» abre la llave con su contraseña y
1085
+ comprueba que es la del certificado, también en el navegador (`verifyEfirmaKey`, con jsrsasign, que se baja hasta que se
1086
+ usa: la llave del SAT va cifrada con 3DES y el navegador no lo trae), y dice el motivo de un «no». Con `onVerifyKey` la
1087
+ revisión la hace su API. Calcita no manda la llave a ningún lado.
1088
+
1089
+ ```jsx
1090
+ import {CfdiSummary, EfirmaFields, clabeRule, readCfdiFile} from '@stackcraftmx/calcita/mexico';
1091
+
1092
+ const cfdi = await readCfdiFile(file); // null si no es factura
1093
+ <CfdiSummary cfdi={cfdi} expectedReceiverRfc={company.rfc} />
1094
+ <EfirmaFields value={efirma} onChange={setEfirma} expectedRfc={company.rfc} /> {/* revisa la llave en el navegador */}
1095
+ ```
1096
+
1097
+ Revisan la forma y leen los archivos; si el RFC existe, si la factura sigue vigente y si la llave abre lo dice su servidor.
1098
+
838
1099
  ## Dinero sin problemas de decimales
839
1100
 
840
1101
  En JavaScript `0.1 + 0.2` da `0.30000000000000004`. `Money` guarda los montos en centavos enteros (como el Money de PHP
@@ -5,9 +5,18 @@
5
5
  import {readFile} from 'node:fs/promises';
6
6
 
7
7
  // Lo que el paquete no usa, así que no está en sus devDependencies: i18next (su adaptador recibe la instancia del proyecto;
8
- // versión con más de dos semanas publicada al escogerla, pnpm espera un día antes de instalar una recién salida) y qrcode
9
- // (el QR de la cuenta de demostración; la misma versión que usan los ejemplos)
10
- const VERSIONS_NOT_IN_PACKAGE = Object.freeze({i18next: '26.4.2', qrcode: '1.5.4'});
8
+ // versión con más de dos semanas publicada al escogerla, pnpm espera un día antes de instalar una recién salida), qrcode
9
+ // (el QR de la cuenta de demostración; la misma versión que usan los ejemplos) y las pruebas de pantalla del proyecto
10
+ // (Vitest con jsdom y Testing Library), en las versiones que ya se probaron juntas con el mismo Vite 7 y React 19
11
+ const VERSIONS_NOT_IN_PACKAGE = Object.freeze({
12
+ i18next: '26.4.2',
13
+ qrcode: '1.5.4',
14
+ vitest: '5.0.3',
15
+ jsdom: '30.1.2',
16
+ '@testing-library/dom': '10.4.2',
17
+ '@testing-library/react': '16.3.3',
18
+ '@testing-library/user-event': '14.6.7',
19
+ });
11
20
 
12
21
  /**
13
22
  * El package.json de Calcita (el del CLI que corre). OJO: por ruta de archivo y no con import: el CLI se publica tal cual,
@@ -15,7 +15,10 @@ const BASE_DEPENDENCIES = Object.freeze([
15
15
  const HOOK_FORM_DEPENDENCIES = Object.freeze(['@hookform/resolvers', 'react-hook-form']);
16
16
  // la cuenta: las llaves de acceso (las pide @stackcraftmx/calcita/account) y el QR de la demostración
17
17
  const ACCOUNT_DEPENDENCIES = Object.freeze(['@simplewebauthn/browser', 'qrcode']);
18
- const DEV_DEPENDENCIES = Object.freeze(['@vitejs/plugin-react', 'vite']);
18
+ // las pruebas de pantalla (pnpm test): Vitest con jsdom como navegador y Testing Library para picar como una persona
19
+ const DEV_DEPENDENCIES = Object.freeze([
20
+ '@testing-library/dom', '@testing-library/react', '@testing-library/user-event', '@vitejs/plugin-react', 'jsdom', 'vite', 'vitest',
21
+ ]);
19
22
 
20
23
  /** Un objeto con sus llaves en orden alfabético (como los escribe npm). */
21
24
  const sortedByKey = (record) => Object.fromEntries(Object.entries(record).sort(([first], [second]) => first.localeCompare(second)));
@@ -40,7 +43,7 @@ export function packageJsonOf({packageName, answers, calcitaPackage}) {
40
43
  version: '0.1.0',
41
44
  private: true,
42
45
  type: 'module',
43
- scripts: {dev: 'vite', build: 'vite build', preview: 'vite preview'},
46
+ scripts: {dev: 'vite', build: 'vite build', preview: 'vite preview', test: 'vitest run'},
44
47
  dependencies: sortedByKey({'@stackcraftmx/calcita': `^${calcitaPackage.version}`, ...versionsOf(dependencyNames, calcitaPackage)}),
45
48
  devDependencies: sortedByKey(versionsOf([...DEV_DEPENDENCIES], calcitaPackage)),
46
49
  engines: {node: '>=22'},
@@ -0,0 +1,24 @@
1
+ // La prueba de pantalla de __LABEL_ES__ (la armó el CLI de Calcita con make:module): la app completa abre en su ruta, con su
2
+ // servidor de mentira, enseña lo que manda el servidor y agrega uno. Cámbiela junto con la pantalla.
3
+ //
4
+ // pnpm test
5
+ import {screen} from '@testing-library/react';
6
+ import {describe, expect, it} from 'vitest';
7
+
8
+ import {__NAME___PATH} from '@/modules/__name__/routes.js';
9
+ import {openScreen} from '@/testing/openScreen.jsx';
10
+
11
+ describe('__LABEL_ES__', () => {
12
+ it('enseña lo que manda el servidor', async () => {
13
+ await openScreen(__NAME___PATH);
14
+ expect(await screen.findByRole('heading', {name: '__LABEL_ES__'})).toBeTruthy();
15
+ expect(await screen.findByText('Primer ejemplo')).toBeTruthy();
16
+ });
17
+
18
+ it('agrega uno y sale en la lista', async () => {
19
+ const {user} = await openScreen(__NAME___PATH);
20
+ await user.type(await screen.findByRole('textbox', {name: 'Nombre'}), 'Uno nuevo');
21
+ await user.click(screen.getByRole('button', {name: 'Guardar'}));
22
+ expect(await screen.findByText('Uno nuevo')).toBeTruthy();
23
+ });
24
+ });
@@ -31,7 +31,8 @@ src/
31
31
  app/ el único lugar donde se conecta el framework: proveedores, marco, rutas, tema y textos
32
32
  core/ lo que comparten dos o más módulos (se importa de '@/core')
33
33
  fakeServer/ el servidor de mentira: datos en memoria con la forma del API de verdad
34
- modules/<módulo>/ cada módulo en su carpeta: menu.js, routes.js, sus pantallas, su *Api.js y sus *Texts.js
34
+ modules/<módulo>/ cada módulo en su carpeta: menu.js, routes.js, sus pantallas, su *Api.js, sus *Texts.js y sus pruebas
35
+ testing/ el kit de las pruebas de pantalla: abrir la app en una dirección (openScreen) y los huecos de jsdom
35
36
  ```
36
37
 
37
38
  - **Un módulo nuevo no edita nada fuera de su carpeta**: el menú, las rutas y los textos se descubren solos.
@@ -42,6 +43,18 @@ src/
42
43
  validaciones) ya vienen.
43
44
  - **Imports con el alias `@/`** (= `src/`), declarado en `vite.config.js` y en `jsconfig.json`.
44
45
 
46
+ ## Pruebas de pantalla
47
+
48
+ ```bash
49
+ pnpm test
50
+ ```
51
+
52
+ Cada `*.test.jsx` abre la app COMPLETA en una dirección (`openScreen`, en `src/testing`), con el servidor de mentira, y pica
53
+ como una persona: busca lo que se ve por su papel y su nombre (`screen.findByRole('heading', {name: 'Bienvenida'})`) y da
54
+ clic o escribe con `user`. Pasa por lo mismo que la persona (permisos, menú, textos, avisos). Vienen las del inicio y las del
55
+ módulo de ejemplo, y `make:module` arma la de cada módulo nuevo. Corren con Vitest y jsdom (un navegador de mentira): lo que
56
+ depende de medir o pintar se revisa en un navegador de verdad.
57
+
45
58
  ## Publicar una versión nueva
46
59
 
47
60
  `pnpm build` deja la app en `dist/`, con `version.json` (la versión de esa compilación). La app publicada lo vuelve a leer al
@@ -28,7 +28,8 @@ const moduleRouteGroups = Object.entries(ROUTE_MODULES)
28
28
  // las pantallas sin el marco (entrar…)
29
29
  const publicRoutes = Object.values(ROUTE_MODULES).flatMap((routeModule) => routeModule.publicRoutes ?? []);
30
30
 
31
- export const appRouter = createBrowserRouter([
31
+ /** Las rutas de la app: el router del navegador (abajo) y el de memoria de las pruebas de pantalla (src/testing). */
32
+ export const appRoutes = [
32
33
  {
33
34
  path: APP_PATHS.home,
34
35
  element: <AppProviders />,
@@ -46,6 +47,8 @@ export const appRouter = createBrowserRouter([
46
47
  },
47
48
  ]),
48
49
  },
50
+ ];
51
+
49
52
  // detrás de un proxy con prefijo (ASSET_URL), el router lo quita al leer la dirección y lo pone en cada liga: las pantallas
50
53
  // siguen escribiendo sus rutas desde la raíz ('/contacts')
51
- ], {basename: ROUTER_BASENAME});
54
+ export const appRouter = createBrowserRouter(appRoutes, {basename: ROUTER_BASENAME});
@@ -20,6 +20,9 @@ export const DEMO_USER = Object.freeze({
20
20
  id: 'user-demo', name: 'Ana Martínez Ruiz', email: 'ana.martinez@ejemplo.com', subtitle: 'Administración', phone: '',
21
21
  });
22
22
 
23
+ /** Dónde recuerda la demostración la sesión en este navegador (con el nombre de la app, para no chocar con otra). */
24
+ export const DEMO_SESSION_STORAGE_KEY = '__APP_SLUG__:session';
25
+
23
26
  /** Sin dirección del API, la app trabaja con la demostración. */
24
27
  export const USES_DEMO_ACCOUNT = !import.meta.env.VITE_API_URL;
25
28
 
@@ -27,8 +30,7 @@ export const accountConnector = USES_DEMO_ACCOUNT
27
30
  ? createDemoAccountConnector({
28
31
  user: DEMO_USER,
29
32
  labels: ['Administración'],
30
- // la sesión de la demostración se recuerda en este navegador, con el nombre de la app para no chocar con otra
31
- storageKey: '__APP_SLUG__:session',
33
+ storageKey: DEMO_SESSION_STORAGE_KEY,
32
34
  issuer: '__APP_TITLE__',
33
35
  // el QR que escanea la app autenticadora
34
36
  toQrImageUrl: (otpauthUrl) => QRCode.toDataURL(otpauthUrl, {margin: 1}),
@@ -0,0 +1,20 @@
1
+ // La prueba de pantalla de entrar: sin sesión, la app manda a «Iniciar sesión» y regresa a donde iba al entrar. Entra con
2
+ // el botón de la cuenta de ejemplo (la demostración): la prueba nunca escribe una contraseña.
3
+ //
4
+ // pnpm test
5
+ import {screen} from '@testing-library/react';
6
+ import {describe, expect, it} from 'vitest';
7
+
8
+ import {openScreen} from '@/testing/openScreen.jsx';
9
+
10
+ describe('entrar', () => {
11
+ it('sin sesión pide entrar y después abre la pantalla que se pidió', async () => {
12
+ const {user, router} = await openScreen('/', {signedIn: false});
13
+ expect(await screen.findByRole('heading', {name: 'Iniciar sesión'})).toBeTruthy();
14
+
15
+ await user.click(screen.getByRole('button', {name: 'Entrar con la cuenta de ejemplo'}));
16
+
17
+ expect(await screen.findByRole('heading', {name: 'Bienvenida'})).toBeTruthy();
18
+ expect(router.state.location.pathname).toBe('/');
19
+ });
20
+ });
@@ -0,0 +1,16 @@
1
+ // La prueba de pantalla del inicio: la app completa abre en «/» y enseña la bienvenida con sus primeros pasos. Es el ejemplo
2
+ // para las demás: abrir la pantalla, buscar lo que ve la persona por su papel y su nombre, y picar como ella.
3
+ //
4
+ // pnpm test
5
+ import {screen} from '@testing-library/react';
6
+ import {describe, expect, it} from 'vitest';
7
+
8
+ import {openScreen} from '@/testing/openScreen.jsx';
9
+
10
+ describe('el inicio', () => {
11
+ it('enseña la bienvenida y cómo seguir con el CLI', async () => {
12
+ await openScreen('/');
13
+ expect(await screen.findByRole('heading', {name: 'Bienvenida'})).toBeTruthy();
14
+ expect(screen.getByText('pnpm exec calcita make:module orders')).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,70 @@
1
+ // Lo que jsdom (el navegador de mentira de las pruebas) no trae y la app sí usa.
2
+ //
3
+ // Cada hueco se llena con lo mínimo para que la pantalla se dibuje. Ninguna prueba depende de que esto haga algo de
4
+ // verdad: jsdom no mide ni pinta, así que tamaños, scroll y medidas de pantalla no se prueban aquí (eso es de un navegador
5
+ // de verdad, con Playwright).
6
+ //
7
+ // Funciones puras, no renderizan: function (regla de la casa).
8
+
9
+ /**
10
+ * Los archivos que la pantalla mandó guardar (una descarga), en orden: {fileName, href}. jsdom no sabe descargar; aquí se
11
+ * anota el nombre para que la prueba revise qué se habría guardado.
12
+ */
13
+ export const savedFiles = [];
14
+
15
+ /** Observador de tamaño o de vista que nunca avisa: en jsdom todo mide 0 y no cambia. */
16
+ class SilentObserver {
17
+ observe() {}
18
+
19
+ unobserve() {}
20
+
21
+ disconnect() {}
22
+
23
+ takeRecords() {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Consulta de medios que nunca coincide: la pantalla se dibuja como en escritorio y en modo claro.
30
+ *
31
+ * @param {string} media la consulta, p. ej. '(prefers-color-scheme: dark)'
32
+ * @returns {MediaQueryList}
33
+ */
34
+ function neverMatchingMediaQuery(media) {
35
+ return {
36
+ matches: false,
37
+ media,
38
+ onchange: null,
39
+ addEventListener() {},
40
+ removeEventListener() {},
41
+ addListener() {},
42
+ removeListener() {},
43
+ dispatchEvent() {
44
+ return false;
45
+ },
46
+ };
47
+ }
48
+
49
+ /** Llena los huecos de jsdom. */
50
+ export function fillBrowserGaps() {
51
+ window.ResizeObserver ??= SilentObserver;
52
+ window.IntersectionObserver ??= SilentObserver;
53
+ window.matchMedia ??= neverMatchingMediaQuery;
54
+ // ir arriba al cambiar de pantalla y llevar a la vista lo elegido
55
+ window.scrollTo = () => {};
56
+ window.HTMLElement.prototype.scrollTo ??= function scrollTo() {};
57
+ window.HTMLElement.prototype.scrollIntoView ??= function scrollIntoView() {};
58
+ // descargas y vistas previas de archivos: una dirección de mentira
59
+ window.URL.createObjectURL ??= () => 'blob:prueba';
60
+ window.URL.revokeObjectURL ??= () => {};
61
+ // la liga de descarga se anota en savedFiles en lugar de navegar (jsdom no navega y lo diría con un error)
62
+ const followLink = window.HTMLAnchorElement.prototype.click;
63
+ window.HTMLAnchorElement.prototype.click = function click() {
64
+ if (this.hasAttribute('download')) {
65
+ savedFiles.push({fileName: this.getAttribute('download'), href: this.href});
66
+ return;
67
+ }
68
+ followLink.call(this);
69
+ };
70
+ }