@stackcraftmx/calcita 0.32.2 → 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.
- package/README.md +263 -2
- package/cli/packageVersions.js +12 -3
- package/cli/projectFiles.js +5 -2
- package/cli/templates/module/__Name__Page.test.jsx.tpl +24 -0
- package/cli/templates/project/README.md.tpl +14 -1
- package/cli/templates/project/src/app/appRouter.jsx.tpl +5 -2
- package/cli/templates/project/src/core/accountConnector.js.tpl +4 -2
- package/cli/templates/project/src/modules/auth/SignInPage.test.jsx.tpl +20 -0
- package/cli/templates/project/src/modules/home/HomePage.test.jsx.tpl +16 -0
- package/cli/templates/project/src/testing/browserGaps.js.tpl +70 -0
- package/cli/templates/project/src/testing/openScreen.jsx.tpl +58 -0
- package/cli/templates/project/src/testing/setup.js.tpl +25 -0
- package/cli/templates/project/vite.config.js.tpl +12 -0
- package/dist/DataTable/GoToPage.js +69 -13
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +2 -0
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/ServerTableFilterDrawer.js +52 -0
- package/dist/DataTable/ServerTableFilterDrawer.js.map +1 -0
- package/dist/DataTable/filterContext.js.map +1 -1
- package/dist/DataTable/goToPageOptions.js +17 -0
- package/dist/DataTable/goToPageOptions.js.map +1 -0
- package/dist/date-pickers.js +28 -2
- package/dist/date-pickers.js.map +1 -1
- package/dist/datePickers/DateInput.js +15 -34
- package/dist/datePickers/DateInput.js.map +1 -1
- package/dist/datePickers/DateTimeInput.js +75 -0
- package/dist/datePickers/DateTimeInput.js.map +1 -0
- package/dist/datePickers/FormDateTimeField.js +12 -0
- package/dist/datePickers/FormDateTimeField.js.map +1 -0
- package/dist/datePickers/FormMonthField.js +12 -0
- package/dist/datePickers/FormMonthField.js.map +1 -0
- package/dist/datePickers/FormTimeField.js +12 -0
- package/dist/datePickers/FormTimeField.js.map +1 -0
- package/dist/datePickers/MonthInput.js +58 -0
- package/dist/datePickers/MonthInput.js.map +1 -0
- package/dist/datePickers/PickerFrame.js +27 -0
- package/dist/datePickers/PickerFrame.js.map +1 -0
- package/dist/datePickers/TimeInput.js +55 -0
- package/dist/datePickers/TimeInput.js.map +1 -0
- package/dist/datePickers/dateTimeRules.js +24 -0
- package/dist/datePickers/dateTimeRules.js.map +1 -0
- package/dist/datePickers/pickerValues.js +106 -0
- package/dist/datePickers/pickerValues.js.map +1 -0
- package/dist/datePickers/useCurrentMinute.js +28 -0
- package/dist/datePickers/useCurrentMinute.js.map +1 -0
- package/dist/datePickers/usePickerField.js +22 -0
- package/dist/datePickers/usePickerField.js.map +1 -0
- package/dist/dictionaries/dictionaries.js +3 -1
- package/dist/dictionaries/dictionaries.js.map +1 -1
- package/dist/forms/FormAttachmentsField.js +12 -0
- package/dist/forms/FormAttachmentsField.js.map +1 -0
- package/dist/forms/FormCheckboxGroupField.js +12 -0
- package/dist/forms/FormCheckboxGroupField.js.map +1 -0
- package/dist/forms/FormFileField.js +12 -0
- package/dist/forms/FormFileField.js.map +1 -0
- package/dist/forms/FormFileUploadField.js +12 -0
- package/dist/forms/FormFileUploadField.js.map +1 -0
- package/dist/forms/FormImageField.js +12 -0
- package/dist/forms/FormImageField.js.map +1 -0
- package/dist/forms/FormMaskedField.js +12 -0
- package/dist/forms/FormMaskedField.js.map +1 -0
- package/dist/forms/FormRepeatingFields.js +54 -0
- package/dist/forms/FormRepeatingFields.js.map +1 -0
- package/dist/forms/FormTransferListField.js +12 -0
- package/dist/forms/FormTransferListField.js.map +1 -0
- package/dist/forms/applyServerProblem.js +10 -3
- package/dist/forms/applyServerProblem.js.map +1 -1
- package/dist/forms/listRules.js +111 -0
- package/dist/forms/listRules.js.map +1 -0
- package/dist/forms/serverProblemFields.js +17 -0
- package/dist/forms/serverProblemFields.js.map +1 -1
- package/dist/forms.js +51 -29
- package/dist/forms.js.map +1 -1
- package/dist/hookForm/FormAttachmentsField.js +12 -0
- package/dist/hookForm/FormAttachmentsField.js.map +1 -0
- package/dist/hookForm/FormCheckboxGroupField.js +12 -0
- package/dist/hookForm/FormCheckboxGroupField.js.map +1 -0
- package/dist/hookForm/FormFileField.js +12 -0
- package/dist/hookForm/FormFileField.js.map +1 -0
- package/dist/hookForm/FormFileUploadField.js +12 -0
- package/dist/hookForm/FormFileUploadField.js.map +1 -0
- package/dist/hookForm/FormImageField.js +12 -0
- package/dist/hookForm/FormImageField.js.map +1 -0
- package/dist/hookForm/FormMaskedField.js +12 -0
- package/dist/hookForm/FormMaskedField.js.map +1 -0
- package/dist/hookForm/FormRepeatingFields.js +44 -0
- package/dist/hookForm/FormRepeatingFields.js.map +1 -0
- package/dist/hookForm/FormTransferListField.js +12 -0
- package/dist/hookForm/FormTransferListField.js.map +1 -0
- package/dist/hookForm/applyServerProblem.js +8 -3
- package/dist/hookForm/applyServerProblem.js.map +1 -1
- package/dist/index.js +188 -100
- package/dist/index.js.map +1 -1
- package/dist/inputs/CatalogInput.js +74 -7
- package/dist/inputs/CatalogInput.js.map +1 -1
- package/dist/inputs/CheckboxGroupInput.js +100 -0
- package/dist/inputs/CheckboxGroupInput.js.map +1 -0
- package/dist/inputs/MaskedInput.js +86 -0
- package/dist/inputs/MaskedInput.js.map +1 -0
- package/dist/inputs/NumberInput.js +12 -1
- package/dist/inputs/NumberInput.js.map +1 -1
- package/dist/inputs/RepeatingFields.js +177 -0
- package/dist/inputs/RepeatingFields.js.map +1 -0
- package/dist/inputs/SelectInput.js +43 -5
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TransferListInput.js +215 -0
- package/dist/inputs/TransferListInput.js.map +1 -0
- package/dist/inputs/inputMasks.js +84 -0
- package/dist/inputs/inputMasks.js.map +1 -0
- package/dist/inputs/selectAllState.js +20 -0
- package/dist/inputs/selectAllState.js.map +1 -0
- package/dist/inputs/useRowKeys.js +26 -0
- package/dist/inputs/useRowKeys.js.map +1 -0
- package/dist/legal/CookieNotice.js +79 -0
- package/dist/legal/CookieNotice.js.map +1 -0
- package/dist/legal/TermsAcceptanceDialog.js +129 -0
- package/dist/legal/TermsAcceptanceDialog.js.map +1 -0
- package/dist/legal/consentStorage.js +18 -0
- package/dist/legal/consentStorage.js.map +1 -0
- package/dist/legal/useCookieConsent.js +17 -0
- package/dist/legal/useCookieConsent.js.map +1 -0
- package/dist/locales/en.js +233 -1
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +233 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/mexico/CfdiSummary.js +81 -0
- package/dist/mexico/CfdiSummary.js.map +1 -0
- package/dist/mexico/EfirmaFields.js +167 -0
- package/dist/mexico/EfirmaFields.js.map +1 -0
- package/dist/mexico/certificateReader.js +79 -0
- package/dist/mexico/certificateReader.js.map +1 -0
- package/dist/mexico/cfdiReader.js +81 -0
- package/dist/mexico/cfdiReader.js.map +1 -0
- package/dist/mexico/derReader.js +81 -0
- package/dist/mexico/derReader.js.map +1 -0
- package/dist/mexico/efirmaKey.js +51 -0
- package/dist/mexico/efirmaKey.js.map +1 -0
- package/dist/mexico/mexicoIdentifiers.js +86 -0
- package/dist/mexico/mexicoIdentifiers.js.map +1 -0
- package/dist/mexico/mexicoRules.js +16 -0
- package/dist/mexico/mexicoRules.js.map +1 -0
- package/dist/mexico.js +32 -0
- package/dist/mexico.js.map +1 -0
- package/dist/react-hook-form.js +40 -17
- package/dist/react-hook-form.js.map +1 -1
- package/dist/rich-text.js +19 -0
- package/dist/rich-text.js.map +1 -0
- package/dist/richText/FormRichTextField.js +12 -0
- package/dist/richText/FormRichTextField.js.map +1 -0
- package/dist/richText/RichTextEditor.js +108 -0
- package/dist/richText/RichTextEditor.js.map +1 -0
- package/dist/richText/RichTextLinkPopover.js +57 -0
- package/dist/richText/RichTextLinkPopover.js.map +1 -0
- package/dist/richText/RichTextToolbar.js +189 -0
- package/dist/richText/RichTextToolbar.js.map +1 -0
- package/dist/richText/RichTextView.js +26 -0
- package/dist/richText/RichTextView.js.map +1 -0
- package/dist/richText/richTextContentSx.js +42 -0
- package/dist/richText/richTextContentSx.js.map +1 -0
- package/dist/richText/richTextExtensions.js +44 -0
- package/dist/richText/richTextExtensions.js.map +1 -0
- package/dist/richText/richTextRules.js +46 -0
- package/dist/richText/richTextRules.js.map +1 -0
- package/dist/table.js +2 -0
- package/dist/table.js.map +1 -1
- package/dist/theme/components/glassComponents.js +27 -0
- package/dist/theme/components/glassComponents.js.map +1 -1
- package/dist/theme/components/minimalComponents.js +12 -0
- package/dist/theme/components/minimalComponents.js.map +1 -1
- package/dist/types/DataTable/ServerTableFilterDrawer.d.ts +21 -0
- package/dist/types/DataTable/filterContext.d.ts +2 -1
- package/dist/types/DataTable/goToPageOptions.d.ts +15 -0
- package/dist/types/DataTable/index.d.ts +1 -0
- package/dist/types/date-pickers.d.ts +10 -1
- package/dist/types/datePickers/DateInput.d.ts +6 -4
- package/dist/types/datePickers/DateTimeInput.d.ts +50 -0
- package/dist/types/datePickers/FormDateTimeField.d.ts +7 -0
- package/dist/types/datePickers/FormMonthField.d.ts +7 -0
- package/dist/types/datePickers/FormTimeField.d.ts +7 -0
- package/dist/types/datePickers/MonthInput.d.ts +39 -0
- package/dist/types/datePickers/PickerFrame.d.ts +22 -0
- package/dist/types/datePickers/TimeInput.d.ts +38 -0
- package/dist/types/datePickers/dateTimeRules.d.ts +31 -0
- package/dist/types/datePickers/pickerValues.d.ts +114 -0
- package/dist/types/datePickers/useCurrentMinute.d.ts +5 -0
- package/dist/types/datePickers/usePickerField.d.ts +24 -0
- package/dist/types/dictionaries/dictionaries.d.ts +6 -3
- package/dist/types/forms/FormAttachmentsField.d.ts +7 -0
- package/dist/types/forms/FormCheckboxGroupField.d.ts +7 -0
- package/dist/types/forms/FormFileField.d.ts +7 -0
- package/dist/types/forms/FormFileUploadField.d.ts +7 -0
- package/dist/types/forms/FormImageField.d.ts +7 -0
- package/dist/types/forms/FormMaskedField.d.ts +7 -0
- package/dist/types/forms/FormRepeatingFields.d.ts +25 -0
- package/dist/types/forms/FormTransferListField.d.ts +7 -0
- package/dist/types/forms/index.d.ts +10 -1
- package/dist/types/forms/listRules.d.ts +48 -0
- package/dist/types/forms/serverProblemFields.d.ts +13 -0
- package/dist/types/forms.d.ts +1 -1
- package/dist/types/hookForm/FormAttachmentsField.d.ts +8 -0
- package/dist/types/hookForm/FormCheckboxGroupField.d.ts +8 -0
- package/dist/types/hookForm/FormFileField.d.ts +8 -0
- package/dist/types/hookForm/FormFileUploadField.d.ts +8 -0
- package/dist/types/hookForm/FormImageField.d.ts +8 -0
- package/dist/types/hookForm/FormMaskedField.d.ts +9 -0
- package/dist/types/hookForm/FormRepeatingFields.d.ts +27 -0
- package/dist/types/hookForm/FormTransferListField.d.ts +8 -0
- package/dist/types/index.d.ts +25 -3
- package/dist/types/inputs/CatalogInput.d.ts +7 -1
- package/dist/types/inputs/CheckboxGroupInput.d.ts +39 -0
- package/dist/types/inputs/MaskedInput.d.ts +35 -0
- package/dist/types/inputs/RepeatingFields.d.ts +52 -0
- package/dist/types/inputs/SelectInput.d.ts +6 -1
- package/dist/types/inputs/TransferListInput.d.ts +43 -0
- package/dist/types/inputs/inputMasks.d.ts +52 -0
- package/dist/types/inputs/selectAllState.d.ts +11 -0
- package/dist/types/inputs/useRowKeys.d.ts +11 -0
- package/dist/types/legal/CookieNotice.d.ts +30 -0
- package/dist/types/legal/TermsAcceptanceDialog.d.ts +32 -0
- package/dist/types/legal/consentStorage.d.ts +16 -0
- package/dist/types/legal/useCookieConsent.d.ts +13 -0
- package/dist/types/locales/en.d.ts +232 -0
- package/dist/types/locales/es.d.ts +232 -0
- package/dist/types/mexico/CfdiSummary.d.ts +17 -0
- package/dist/types/mexico/EfirmaFields.d.ts +38 -0
- package/dist/types/mexico/certificateReader.d.ts +37 -0
- package/dist/types/mexico/cfdiReader.d.ts +95 -0
- package/dist/types/mexico/derReader.d.ts +54 -0
- package/dist/types/mexico/efirmaKey.d.ts +26 -0
- package/dist/types/mexico/mexicoIdentifiers.d.ts +61 -0
- package/dist/types/mexico/mexicoRules.d.ts +29 -0
- package/dist/types/mexico.d.ts +11 -0
- package/dist/types/react-hook-form.d.ts +10 -1
- package/dist/types/rich-text.d.ts +5 -0
- package/dist/types/richText/FormRichTextField.d.ts +7 -0
- package/dist/types/richText/RichTextEditor.d.ts +42 -0
- package/dist/types/richText/RichTextLinkPopover.d.ts +17 -0
- package/dist/types/richText/RichTextToolbar.d.ts +15 -0
- package/dist/types/richText/RichTextView.d.ts +13 -0
- package/dist/types/richText/richTextContentSx.d.ts +7 -0
- package/dist/types/richText/richTextExtensions.d.ts +15 -0
- package/dist/types/richText/richTextRules.d.ts +41 -0
- package/dist/types/table.d.ts +1 -1
- package/dist/types/ui/actions/ActionsMenu.d.ts +18 -3
- package/dist/types/ui/actions/CopyButton.d.ts +7 -4
- package/dist/types/ui/actions/tableRowText.d.ts +6 -0
- package/dist/types/ui/cards/RecordCard.d.ts +65 -0
- package/dist/types/ui/cards/RecordCardSkeleton.d.ts +17 -0
- package/dist/types/ui/countdown/Countdown.d.ts +31 -0
- package/dist/types/ui/countdown/countdownParts.d.ts +39 -0
- package/dist/types/ui/countdown/useCountdown.d.ts +14 -0
- package/dist/types/ui/documents/PdfViewerDialog.d.ts +21 -0
- package/dist/types/ui/filters/FilterDrawer.d.ts +34 -0
- package/dist/types/ui/filters/filterDrawerFields.d.ts +139 -0
- package/dist/types/ui/help/HelpButton.d.ts +15 -0
- package/dist/types/ui/help/HelpCarouselDialog.d.ts +35 -0
- package/dist/types/ui/help/useHelpCarousel.d.ts +21 -0
- package/dist/types/ui/index.d.ts +10 -0
- package/dist/types/ui/labels/IconBadge.d.ts +34 -0
- package/dist/types/ui/navigation/RouteTabs.d.ts +44 -0
- package/dist/types/ui/navigation/routeTabMatching.d.ts +27 -0
- package/dist/types/upload/AttachmentsInput.d.ts +62 -0
- package/dist/types/upload/FileChip.d.ts +18 -0
- package/dist/types/upload/FileDropzone.d.ts +3 -0
- package/dist/types/upload/FileInput.d.ts +48 -0
- package/dist/types/upload/FileUploadInput.d.ts +80 -0
- package/dist/types/upload/ImageInput.d.ts +53 -0
- package/dist/types/upload/TemplateDownloadButton.d.ts +39 -0
- package/dist/types/upload/fileAppearance.d.ts +8 -0
- package/dist/types/upload/fileRules.d.ts +38 -0
- package/dist/types/upload/transferProgress.d.ts +18 -0
- package/dist/types/upload/uploadMachine.d.ts +6 -2
- package/dist/types/upload/uploadWithProgress.d.ts +6 -2
- package/dist/types/upload/useFilePicker.d.ts +39 -0
- package/dist/ui/actions/ActionsMenu.js +98 -15
- package/dist/ui/actions/ActionsMenu.js.map +1 -1
- package/dist/ui/actions/CopyButton.js +31 -11
- package/dist/ui/actions/CopyButton.js.map +1 -1
- package/dist/ui/actions/tableRowText.js +7 -0
- package/dist/ui/actions/tableRowText.js.map +1 -0
- package/dist/ui/cards/RecordCard.js +145 -0
- package/dist/ui/cards/RecordCard.js.map +1 -0
- package/dist/ui/cards/RecordCardSkeleton.js +33 -0
- package/dist/ui/cards/RecordCardSkeleton.js.map +1 -0
- package/dist/ui/countdown/Countdown.js +38 -0
- package/dist/ui/countdown/Countdown.js.map +1 -0
- package/dist/ui/countdown/countdownParts.js +34 -0
- package/dist/ui/countdown/countdownParts.js.map +1 -0
- package/dist/ui/countdown/useCountdown.js +35 -0
- package/dist/ui/countdown/useCountdown.js.map +1 -0
- package/dist/ui/documents/PdfViewerDialog.js +81 -0
- package/dist/ui/documents/PdfViewerDialog.js.map +1 -0
- package/dist/ui/filters/FilterDrawer.js +211 -0
- package/dist/ui/filters/FilterDrawer.js.map +1 -0
- package/dist/ui/filters/filterDrawerFields.js +119 -0
- package/dist/ui/filters/filterDrawerFields.js.map +1 -0
- package/dist/ui/help/HelpButton.js +17 -0
- package/dist/ui/help/HelpButton.js.map +1 -0
- package/dist/ui/help/HelpCarouselDialog.js +154 -0
- package/dist/ui/help/HelpCarouselDialog.js.map +1 -0
- package/dist/ui/help/useHelpCarousel.js +25 -0
- package/dist/ui/help/useHelpCarousel.js.map +1 -0
- package/dist/ui/labels/IconBadge.js +82 -0
- package/dist/ui/labels/IconBadge.js.map +1 -0
- package/dist/ui/navigation/RouteTabs.js +75 -0
- package/dist/ui/navigation/RouteTabs.js.map +1 -0
- package/dist/ui/navigation/routeTabMatching.js +23 -0
- package/dist/ui/navigation/routeTabMatching.js.map +1 -0
- package/dist/upload/AttachmentsInput.js +127 -0
- package/dist/upload/AttachmentsInput.js.map +1 -0
- package/dist/upload/FileChip.js +27 -0
- package/dist/upload/FileChip.js.map +1 -0
- package/dist/upload/FileDropzone.js +19 -84
- package/dist/upload/FileDropzone.js.map +1 -1
- package/dist/upload/FileInput.js +112 -0
- package/dist/upload/FileInput.js.map +1 -0
- package/dist/upload/FileUploadInput.js +285 -0
- package/dist/upload/FileUploadInput.js.map +1 -0
- package/dist/upload/ImageInput.js +125 -0
- package/dist/upload/ImageInput.js.map +1 -0
- package/dist/upload/TemplateDownloadButton.js +67 -0
- package/dist/upload/TemplateDownloadButton.js.map +1 -0
- package/dist/upload/fileAppearance.js +21 -0
- package/dist/upload/fileAppearance.js.map +1 -0
- package/dist/upload/fileRules.js +27 -1
- package/dist/upload/fileRules.js.map +1 -1
- package/dist/upload/transferProgress.js +23 -0
- package/dist/upload/transferProgress.js.map +1 -0
- package/dist/upload/uploadMachine.js +8 -8
- package/dist/upload/uploadMachine.js.map +1 -1
- package/dist/upload/uploadWithProgress.js +1 -1
- package/dist/upload/uploadWithProgress.js.map +1 -1
- package/dist/upload/useFilePicker.js +101 -0
- package/dist/upload/useFilePicker.js.map +1 -0
- 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` |
|
|
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
|
package/cli/packageVersions.js
CHANGED
|
@@ -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)
|
|
9
|
-
// (el QR de la cuenta de demostración; la misma versión que usan los ejemplos)
|
|
10
|
-
|
|
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,
|
package/cli/projectFiles.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|