@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useHelpCarousel.js","sources":["../../../src/ui/help/useHelpCarousel.js"],"sourcesContent":["// Cuándo sale la ayuda en carrusel (HelpCarouselDialog): sola la primera vez que se entra, y otra vez cuando cambia\n// (versión nueva), hasta que la persona la termina o marca «No volver a mostrar». Se recuerda en este navegador; si el\n// proyecto ya sabe en su servidor que la persona la vio (como el «primeras instrucciones» de un usuario), lo pasa en seen\n// y manda.\n//\n// const help = useHelpCarousel({helpKey: 'tasks', version: '2'});\n// <HelpButton onClick={help.openHelp} />\n// <HelpCarouselDialog open={help.open} onClose={help.closeHelp} steps={…} />\n//\n// Hook genérico: function (regla de la casa).\nimport {useState} from 'react';\n\nimport {acceptedVersionOf, rememberAcceptedVersion} from '@/legal/consentStorage.js';\n\nconst STORAGE_PREFIX = 'calcita:help:';\n\n/**\n * @param {object} options\n * @param {string} options.helpKey de qué pantalla es la ayuda ('tasks'): una llave por ayuda\n * @param {string} [options.version] súbala cuando la ayuda cambie y deba volver a salir\n * @param {boolean} [options.autoOpen] que salga sola la primera vez (por omisión sí)\n * @param {boolean} [options.seen] lo que sabe el servidor del proyecto: true = ya la vio (no sale sola)\n * @returns {{open: boolean, openHelp: () => void, closeHelp: (reason?: string, details?: {dontShowAgain: boolean}) => void, wasSeen: boolean}}\n */\nexport function useHelpCarousel({helpKey, version = '1', autoOpen = true, seen = false}) {\n const storageKey = `${STORAGE_PREFIX}${helpKey}`;\n const wasSeen = seen || acceptedVersionOf(storageKey) === version;\n const [open, setOpen] = useState(() => autoOpen && !wasSeen);\n\n return {\n open,\n wasSeen,\n openHelp: () => setOpen(true),\n // terminarla la recuerda; cerrarla antes, solo con «No volver a mostrar»\n closeHelp: (reason, {dontShowAgain = false} = {}) => {\n setOpen(false);\n if (reason === 'finished' || dontShowAgain) {\n rememberAcceptedVersion(storageKey, version);\n }\n },\n };\n}\n"],"names":[],"mappings":";;;AAcA,MAAM,iBAAiB;AAUhB,SAAS,gBAAgB,EAAC,SAAS,UAAU,KAAK,WAAW,MAAM,OAAO,MAAK,GAAG;AACrF,QAAM,aAAa,GAAG,cAAc,GAAG,OAAO;AAC9C,QAAM,UAAU,QAAQ,kBAAkB,UAAU,MAAM;AAC1D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,YAAY,CAAC,OAAO;AAE3D,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,UAAU,MAAM,QAAQ,IAAI;AAAA;AAAA,IAE5B,WAAW,CAAC,QAAQ,EAAC,gBAAgB,MAAK,IAAI,CAAA,MAAO;AACjD,cAAQ,KAAK;AACb,UAAI,WAAW,cAAc,eAAe;AACxC,gCAAwB,YAAY,OAAO;AAAA,MAC/C;AAAA,IACJ;AAAA,EACR;AACA;"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
4
|
+
import { styled, useThemeProps } from "@mui/material/styles";
|
|
5
|
+
import { lookup, labelOf } from "../../dictionaries/dictionaries.js";
|
|
6
|
+
import { paletteOf } from "../../theme/components/paletteOf.js";
|
|
7
|
+
import { accentTextStylesOf } from "../../theme/glassSurface.js";
|
|
8
|
+
import { useToolkitTranslation } from "../../toolkit/useToolkitTranslation.js";
|
|
9
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
10
|
+
const ICON_BADGE_VARIANT = Object.freeze({ outlined: "outlined", soft: "soft", plain: "plain" });
|
|
11
|
+
const PALETTE_TONES = ["primary", "secondary", "success", "warning", "error", "info"];
|
|
12
|
+
const SOFT_FILL_PERCENT = 10;
|
|
13
|
+
const SIZES = Object.freeze({
|
|
14
|
+
small: { fontSize: 12, iconSize: 16, paddingBlock: 2, paddingInline: 8, gap: 4 },
|
|
15
|
+
medium: { fontSize: 14, iconSize: 20, paddingBlock: 5, paddingInline: 10, gap: 6 }
|
|
16
|
+
});
|
|
17
|
+
const THEME_NAME = "CalcitaIconBadge";
|
|
18
|
+
const ICON_BADGE_CLASSES = Object.freeze({ root: `${THEME_NAME}-root`, icon: `${THEME_NAME}-icon`, label: `${THEME_NAME}-label` });
|
|
19
|
+
const IconBadgeRoot = styled("span", { name: THEME_NAME, slot: "Root" })(({ theme, ownerState }) => {
|
|
20
|
+
const palette = paletteOf(theme);
|
|
21
|
+
const tone = PALETTE_TONES.includes(ownerState.tone) ? palette[ownerState.tone] : null;
|
|
22
|
+
const size = SIZES[ownerState.size] ?? SIZES.small;
|
|
23
|
+
const isBoxed = ownerState.variant !== ICON_BADGE_VARIANT.plain;
|
|
24
|
+
const textColors = tone ? { ...accentTextStylesOf(theme, ownerState.tone), borderColor: "currentColor" } : { color: palette.text.secondary, borderColor: palette.divider };
|
|
25
|
+
return {
|
|
26
|
+
display: "inline-flex",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
gap: size.gap,
|
|
29
|
+
maxWidth: "100%",
|
|
30
|
+
verticalAlign: "middle",
|
|
31
|
+
fontSize: size.fontSize,
|
|
32
|
+
fontWeight: 600,
|
|
33
|
+
lineHeight: 1.4,
|
|
34
|
+
whiteSpace: "nowrap",
|
|
35
|
+
...textColors,
|
|
36
|
+
...isBoxed && {
|
|
37
|
+
paddingBlock: size.paddingBlock,
|
|
38
|
+
paddingInline: size.paddingInline,
|
|
39
|
+
borderRadius: theme.shape.borderRadius,
|
|
40
|
+
backgroundColor: `color-mix(in srgb, ${tone?.main ?? palette.text.secondary} ${SOFT_FILL_PERCENT}%, transparent)`,
|
|
41
|
+
borderWidth: ownerState.variant === ICON_BADGE_VARIANT.outlined ? 1.5 : 0,
|
|
42
|
+
borderStyle: "solid"
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const IconBadgeIcon = styled("span", { name: THEME_NAME, slot: "Icon" })(({ ownerState }) => {
|
|
47
|
+
const size = SIZES[ownerState.size] ?? SIZES.small;
|
|
48
|
+
return { display: "inline-flex", flexShrink: 0, "& svg": { fontSize: size.iconSize } };
|
|
49
|
+
});
|
|
50
|
+
const IconBadgeLabel = styled("span", { name: THEME_NAME, slot: "Label" })({ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" });
|
|
51
|
+
function IconBadge(props) {
|
|
52
|
+
const {
|
|
53
|
+
dictionary,
|
|
54
|
+
value,
|
|
55
|
+
icon,
|
|
56
|
+
label,
|
|
57
|
+
tone = "primary",
|
|
58
|
+
variant = ICON_BADGE_VARIANT.outlined,
|
|
59
|
+
size = "small",
|
|
60
|
+
tooltip,
|
|
61
|
+
sx,
|
|
62
|
+
...unknownProps
|
|
63
|
+
} = useThemeProps({ props, name: THEME_NAME });
|
|
64
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
65
|
+
const { t } = useToolkitTranslation("dictionaries");
|
|
66
|
+
const entry = dictionary ? lookup(dictionary, value) : null;
|
|
67
|
+
const Icon = entry?.icon ?? icon;
|
|
68
|
+
const text = entry ? labelOf(entry, t) : label;
|
|
69
|
+
const explanation = tooltip ?? (entry ? t(entry.tooltipKey, { defaultValue: "" }) : null);
|
|
70
|
+
const ownerState = { tone: entry?.color ?? tone, variant, size };
|
|
71
|
+
const badge = /* @__PURE__ */ jsxs(IconBadgeRoot, { ownerState, className: ICON_BADGE_CLASSES.root, sx, children: [
|
|
72
|
+
Icon && /* @__PURE__ */ jsx(IconBadgeIcon, { ownerState, className: ICON_BADGE_CLASSES.icon, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, {}) }),
|
|
73
|
+
/* @__PURE__ */ jsx(IconBadgeLabel, { ownerState, className: ICON_BADGE_CLASSES.label, children: text })
|
|
74
|
+
] });
|
|
75
|
+
return explanation ? /* @__PURE__ */ jsx(Tooltip, { title: explanation, children: badge }) : badge;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
ICON_BADGE_CLASSES,
|
|
79
|
+
ICON_BADGE_VARIANT,
|
|
80
|
+
IconBadge as default
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=IconBadge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IconBadge.js","sources":["../../../src/ui/labels/IconBadge.jsx"],"sourcesContent":["// La insignia: un ícono y un texto corto con el color de lo que significan («Envío asegurado», «Pago con tarjeta», «Requiere\n// factura»). Para destacar una característica de un registro en su encabezado o en su tarjeta; no es un botón.\n//\n// Tres estilos (variant):\n// outlined borde de su color y fondo tenue (por omisión): resalta en una tarjeta\n// soft solo el fondo tenue, sin borde: varias juntas sin ruido\n// plain sin caja, el ícono y el texto de su color: dentro de un renglón de texto\n// El texto va en su color oscurecido en claro y aclarado en oscuro, lo justo para leerse (accentTextStylesOf, la regla del\n// tema; con el tono «dark» de la paleta, el azul sin caja daba 4.28 : 1 sobre una tarjeta, medido).\n//\n// Desde un diccionario (la regla de la casa: etiqueta, tooltip, color e ícono salen de ahí): dictionary + value; la entrada\n// trae su ícono con dictionaryEntryOf(…, {icon}). O suelta: icon, label y tone.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaIconBadge con defaultProps y\n// styleOverrides de sus partes: root, icon, label.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Tooltip from '@mui/material/Tooltip';\nimport {styled, useThemeProps} from '@mui/material/styles';\n\nimport {labelOf, lookup} from '@/dictionaries/dictionaries.js';\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {accentTextStylesOf} from '@/theme/glassSurface.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n/** Los estilos de la insignia. */\nexport const ICON_BADGE_VARIANT = Object.freeze({outlined: 'outlined', soft: 'soft', plain: 'plain'});\n\n// los colores de la paleta que se pueden usar; otro (o «default») se pinta neutro, en gris\nconst PALETTE_TONES = ['primary', 'secondary', 'success', 'warning', 'error', 'info'];\n// cuánto del color lleva el fondo tenue\nconst SOFT_FILL_PERCENT = 10;\nconst SIZES = Object.freeze({\n small: {fontSize: 12, iconSize: 16, paddingBlock: 2, paddingInline: 8, gap: 4},\n medium: {fontSize: 14, iconSize: 20, paddingBlock: 5, paddingInline: 10, gap: 6},\n});\n\n// el nombre de la pieza en el tema (theme.components.CalcitaIconBadge) y sus clases fijas, para el CSS de un proyecto o una\n// prueba (como las de MUI: el nombre que pone styled no sale en la versión construida)\nconst THEME_NAME = 'CalcitaIconBadge';\nexport const ICON_BADGE_CLASSES = Object.freeze({root: `${THEME_NAME}-root`, icon: `${THEME_NAME}-icon`, label: `${THEME_NAME}-label`});\n\nconst IconBadgeRoot = styled('span', {name: THEME_NAME, slot: 'Root'})(({theme, ownerState}) => {\n const palette = paletteOf(theme);\n const tone = PALETTE_TONES.includes(ownerState.tone) ? palette[ownerState.tone] : null;\n const size = SIZES[ownerState.size] ?? SIZES.small;\n const isBoxed = ownerState.variant !== ICON_BADGE_VARIANT.plain;\n // el texto y el borde: su color oscurecido en claro y aclarado en oscuro (borderColor: currentColor toma el mismo)\n const textColors = tone\n ? {...accentTextStylesOf(theme, ownerState.tone), borderColor: 'currentColor'}\n : {color: palette.text.secondary, borderColor: palette.divider};\n return {\n display: 'inline-flex', alignItems: 'center', gap: size.gap, maxWidth: '100%', verticalAlign: 'middle',\n fontSize: size.fontSize, fontWeight: 600, lineHeight: 1.4, whiteSpace: 'nowrap',\n ...textColors,\n ...(isBoxed && {\n paddingBlock: size.paddingBlock, paddingInline: size.paddingInline, borderRadius: theme.shape.borderRadius,\n backgroundColor: `color-mix(in srgb, ${tone?.main ?? palette.text.secondary} ${SOFT_FILL_PERCENT}%, transparent)`,\n borderWidth: ownerState.variant === ICON_BADGE_VARIANT.outlined ? 1.5 : 0, borderStyle: 'solid',\n }),\n };\n});\nconst IconBadgeIcon = styled('span', {name: THEME_NAME, slot: 'Icon'})(({ownerState}) => {\n const size = SIZES[ownerState.size] ?? SIZES.small;\n return {display: 'inline-flex', flexShrink: 0, '& svg': {fontSize: size.iconSize}};\n});\n// el texto en su propio span: un nodo de texto suelto dentro de un flex se corta sin «…»\nconst IconBadgeLabel = styled('span', {name: THEME_NAME, slot: 'Label'})({minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis'});\n\n/**\n * @param {object} props\n * @param {Record<string, object>} [props.dictionary] de donde sale la etiqueta, el tooltip, el color y el ícono\n * @param {string|null} [props.value] con dictionary: el código del API\n * @param {import('react').ElementType} [props.icon] suelta: el ícono (un componente, p. ej. VerifiedUserOutlinedIcon)\n * @param {import('react').ReactNode} [props.label] suelta: el texto, ya traducido\n * @param {'primary'|'secondary'|'success'|'warning'|'error'|'info'|'default'} [props.tone] suelta: el color\n * @param {'outlined'|'soft'|'plain'} [props.variant] ICON_BADGE_VARIANT\n * @param {'small'|'medium'} [props.size]\n * @param {import('react').ReactNode} [props.tooltip] lo que explica al pasar el cursor (con dictionary, el del diccionario)\n * @param {object} [props.sx]\n */\nexport default function IconBadge(props) {\n const {\n dictionary, value, icon, label, tone = 'primary', variant = ICON_BADGE_VARIANT.outlined, size = 'small', tooltip, sx,\n ...unknownProps\n } = useThemeProps({props, name: THEME_NAME});\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('dictionaries');\n const entry = dictionary ? lookup(dictionary, value) : null;\n const Icon = entry?.icon ?? icon;\n const text = entry ? labelOf(entry, t) : label;\n const explanation = tooltip ?? (entry ? t(entry.tooltipKey, {defaultValue: ''}) : null);\n const ownerState = {tone: entry?.color ?? tone, variant, size};\n\n const badge = (\n <IconBadgeRoot ownerState={ownerState} className={ICON_BADGE_CLASSES.root} sx={sx}>\n {Icon && <IconBadgeIcon ownerState={ownerState} className={ICON_BADGE_CLASSES.icon} aria-hidden=\"true\"><Icon /></IconBadgeIcon>}\n <IconBadgeLabel ownerState={ownerState} className={ICON_BADGE_CLASSES.label}>{text}</IconBadgeLabel>\n </IconBadgeRoot>\n );\n // sin explicación no se envuelve (un Tooltip vacío igual agrega listeners)\n return explanation ? <Tooltip title={explanation}>{badge}</Tooltip> : badge;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Chip from "@mui/material/Chip";
|
|
4
|
+
import Stack from "@mui/material/Stack";
|
|
5
|
+
import Tab from "@mui/material/Tab";
|
|
6
|
+
import Tabs from "@mui/material/Tabs";
|
|
7
|
+
import { useAccessChecker } from "../../access/useCan.js";
|
|
8
|
+
import { piecePart, usePieceProps } from "../../theme/themePieces.js";
|
|
9
|
+
import { useToolkitNavigation } from "../../toolkit/toolkitContext.js";
|
|
10
|
+
import { warnUnknownProps } from "../../toolkit/unknownProps.js";
|
|
11
|
+
import { queryTabOf, activeRouteTabOf, searchWithTab } from "./routeTabMatching.js";
|
|
12
|
+
const ROUTE_TABS_MODE = Object.freeze({ path: "path", query: "query" });
|
|
13
|
+
const DEFAULT_PARAM_NAME = "tab";
|
|
14
|
+
const THEME_NAME = "CalcitaRouteTabs";
|
|
15
|
+
const RouteTabsRoot = piecePart(THEME_NAME, "Root", Tabs);
|
|
16
|
+
const RouteTab = piecePart(THEME_NAME, "Tab", Tab);
|
|
17
|
+
const RouteTabBadge = piecePart(THEME_NAME, "Badge", Chip);
|
|
18
|
+
const visibleTabsOf = (tabs, canAccess) => tabs.filter((tab) => !tab.hidden && canAccess(tab.access));
|
|
19
|
+
function useActiveRouteTab(tabs, { mode = ROUTE_TABS_MODE.path, paramName = DEFAULT_PARAM_NAME } = {}) {
|
|
20
|
+
const { useLocation } = useToolkitNavigation();
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
const visibleTabs = visibleTabsOf(tabs, useAccessChecker());
|
|
23
|
+
return mode === ROUTE_TABS_MODE.query ? queryTabOf(visibleTabs, location.search, paramName) : activeRouteTabOf(visibleTabs, location.pathname);
|
|
24
|
+
}
|
|
25
|
+
function RouteTabs(props) {
|
|
26
|
+
const { tabs, ariaLabel, mode = ROUTE_TABS_MODE.path, paramName = DEFAULT_PARAM_NAME, onChange, sx, ...unknownProps } = usePieceProps(props, THEME_NAME);
|
|
27
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
28
|
+
const { useLocation, Link } = useToolkitNavigation();
|
|
29
|
+
const location = useLocation();
|
|
30
|
+
const visibleTabs = visibleTabsOf(tabs, useAccessChecker());
|
|
31
|
+
const isQuery = mode === ROUTE_TABS_MODE.query;
|
|
32
|
+
const activeKey = isQuery ? queryTabOf(visibleTabs, location.search, paramName) : activeRouteTabOf(visibleTabs, location.pathname);
|
|
33
|
+
return (
|
|
34
|
+
// value false: ninguna pestaña coincide (MUI avisa en la consola si el valor no es de ninguna)
|
|
35
|
+
/* @__PURE__ */ jsx(
|
|
36
|
+
RouteTabsRoot,
|
|
37
|
+
{
|
|
38
|
+
value: activeKey ?? false,
|
|
39
|
+
variant: "scrollable",
|
|
40
|
+
allowScrollButtonsMobile: true,
|
|
41
|
+
"aria-label": ariaLabel,
|
|
42
|
+
ownSx: { minWidth: 0, ...sx },
|
|
43
|
+
children: visibleTabs.map((tab) => {
|
|
44
|
+
const key = isQuery ? tab.value : tab.to;
|
|
45
|
+
const href = isQuery ? `${location.pathname}${searchWithTab(location.search, paramName, tab.value)}` : tab.to;
|
|
46
|
+
const label = tab.badge === void 0 || tab.badge === null ? tab.label : /* @__PURE__ */ jsxs(Stack, { component: "span", direction: "row", sx: { alignItems: "center", gap: 0.75 }, children: [
|
|
47
|
+
/* @__PURE__ */ jsx("span", { children: tab.label }),
|
|
48
|
+
/* @__PURE__ */ jsx(RouteTabBadge, { size: "small", label: tab.badge, ownSx: { height: 20, fontSize: 12, fontWeight: 600 } })
|
|
49
|
+
] });
|
|
50
|
+
return /* @__PURE__ */ jsx(
|
|
51
|
+
RouteTab,
|
|
52
|
+
{
|
|
53
|
+
value: key,
|
|
54
|
+
label,
|
|
55
|
+
icon: tab.icon,
|
|
56
|
+
iconPosition: "start",
|
|
57
|
+
disabled: tab.disabled,
|
|
58
|
+
component: Link,
|
|
59
|
+
to: href,
|
|
60
|
+
onClick: () => onChange?.(tab),
|
|
61
|
+
ownSx: { minHeight: 48 }
|
|
62
|
+
},
|
|
63
|
+
key
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
ROUTE_TABS_MODE,
|
|
72
|
+
RouteTabs as default,
|
|
73
|
+
useActiveRouteTab
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=RouteTabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteTabs.js","sources":["../../../src/ui/navigation/RouteTabs.jsx"],"sourcesContent":["// Pestañas por ruta: cada pestaña es una dirección, así se puede compartir la liga a una pestaña, regresar con «atrás» y\n// abrirla en otra pestaña del navegador. Dos modos (mode):\n// path cada pestaña es una página de las rutas del proyecto (to); la activa sale de la dirección\n// query pestañas dentro de una misma pantalla, guardadas en la consulta (?tab=pagos); useActiveRouteTab dice cuál pintar\n//\n// Cada pestaña puede traer su ícono, un conteo (badge), su permiso (access, con la regla de ToolkitProvider: la que no se\n// puede ver no sale) o estar apagada. Usa la navegación que conectó el proyecto (react-router, Next o la del navegador).\n// En el teléfono se deslizan de lado.\n//\n// <RouteTabs ariaLabel=\"Secciones del cliente\" tabs={[\n// {to: `/customers/${id}`, label: 'Resumen', exact: true},\n// {to: `/customers/${id}/payments`, label: 'Pagos', badge: 3},\n// {to: `/customers/${id}/files`, label: 'Documentos', access: {ability: 'files.view'}},\n// ]} />\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaRouteTabs con defaultProps y\n// styleOverrides de sus partes: root, tab, badge.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Chip from '@mui/material/Chip';\nimport Stack from '@mui/material/Stack';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\n\nimport {useAccessChecker} from '@/access/useCan.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {activeRouteTabOf, queryTabOf, searchWithTab} from '@/ui/navigation/routeTabMatching.js';\n\n/** Dónde vive la pestaña: en la ruta o en la consulta. */\nexport const ROUTE_TABS_MODE = Object.freeze({path: 'path', query: 'query'});\nconst DEFAULT_PARAM_NAME = 'tab';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaRouteTabs)\nconst THEME_NAME = 'CalcitaRouteTabs';\nconst RouteTabsRoot = piecePart(THEME_NAME, 'Root', Tabs);\nconst RouteTab = piecePart(THEME_NAME, 'Tab', Tab);\nconst RouteTabBadge = piecePart(THEME_NAME, 'Badge', Chip);\n\n/**\n * Las pestañas que esta sesión puede ver.\n *\n * @param {object[]} tabs\n * @param {(access: object|undefined) => boolean} canAccess\n * @returns {object[]}\n */\nconst visibleTabsOf = (tabs, canAccess) => tabs.filter((tab) => !tab.hidden && canAccess(tab.access));\n\n/**\n * La pestaña activa, para que la pantalla pinte lo suyo (sobre todo con mode query: la pantalla es una sola).\n *\n * @param {object[]} tabs las mismas de RouteTabs\n * @param {{mode?: string, paramName?: string}} [options]\n * @returns {string|null} el `to` (por ruta) o el `value` (por consulta) de la activa\n */\nexport function useActiveRouteTab(tabs, {mode = ROUTE_TABS_MODE.path, paramName = DEFAULT_PARAM_NAME} = {}) {\n const {useLocation} = useToolkitNavigation();\n const location = useLocation();\n const visibleTabs = visibleTabsOf(tabs, useAccessChecker());\n return mode === ROUTE_TABS_MODE.query\n ? queryTabOf(visibleTabs, location.search, paramName)\n : activeRouteTabOf(visibleTabs, location.pathname);\n}\n\n/**\n * @param {object} props\n * @param {Array<{to?: string, value?: string, label: import('react').ReactNode, icon?: import('react').ReactElement, badge?: import('react').ReactNode, access?: object, exact?: boolean, disabled?: boolean, hidden?: boolean}>} props.tabs\n * to (por ruta) o value (por consulta); label ya traducido; badge: un conteo o un texto corto junto a la etiqueta\n * @param {string} props.ariaLabel ya traducido: de qué son estas pestañas (para el lector de pantalla)\n * @param {'path'|'query'} [props.mode] ROUTE_TABS_MODE\n * @param {string} [props.paramName] con mode query: el nombre en la consulta (por omisión 'tab')\n * @param {(tab: object) => void} [props.onChange] avisa la pestaña que se eligió (la navegación ya la hace la liga)\n * @param {object} [props.sx]\n */\nexport default function RouteTabs(props) {\n const {tabs, ariaLabel, mode = ROUTE_TABS_MODE.path, paramName = DEFAULT_PARAM_NAME, onChange, sx, ...unknownProps} = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {useLocation, Link} = useToolkitNavigation();\n const location = useLocation();\n const visibleTabs = visibleTabsOf(tabs, useAccessChecker());\n const isQuery = mode === ROUTE_TABS_MODE.query;\n const activeKey = isQuery ? queryTabOf(visibleTabs, location.search, paramName) : activeRouteTabOf(visibleTabs, location.pathname);\n\n return (\n // value false: ninguna pestaña coincide (MUI avisa en la consola si el valor no es de ninguna)\n <RouteTabsRoot value={activeKey ?? false} variant=\"scrollable\" allowScrollButtonsMobile aria-label={ariaLabel}\n ownSx={{minWidth: 0, ...sx}}>\n {visibleTabs.map((tab) => {\n const key = isQuery ? tab.value : tab.to;\n const href = isQuery ? `${location.pathname}${searchWithTab(location.search, paramName, tab.value)}` : tab.to;\n const label = tab.badge === undefined || tab.badge === null ? tab.label : (\n <Stack component=\"span\" direction=\"row\" sx={{alignItems: 'center', gap: 0.75}}>\n <span>{tab.label}</span>\n <RouteTabBadge size=\"small\" label={tab.badge} ownSx={{height: 20, fontSize: 12, fontWeight: 600}} />\n </Stack>\n );\n return (\n <RouteTab key={key} value={key} label={label} icon={tab.icon} iconPosition=\"start\" disabled={tab.disabled}\n component={Link} to={href} onClick={() => onChange?.(tab)} ownSx={{minHeight: 48}} />\n );\n })}\n </RouteTabsRoot>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA+BO,MAAM,kBAAkB,OAAO,OAAO,EAAC,MAAM,QAAQ,OAAO,SAAQ;AAC3E,MAAM,qBAAqB;AAG3B,MAAM,aAAa;AACnB,MAAM,gBAAgB,UAAU,YAAY,QAAQ,IAAI;AACxD,MAAM,WAAW,UAAU,YAAY,OAAO,GAAG;AACjD,MAAM,gBAAgB,UAAU,YAAY,SAAS,IAAI;AASzD,MAAM,gBAAgB,CAAC,MAAM,cAAc,KAAK,OAAO,CAAC,QAAQ,CAAC,IAAI,UAAU,UAAU,IAAI,MAAM,CAAC;AAS7F,SAAS,kBAAkB,MAAM,EAAC,OAAO,gBAAgB,MAAM,YAAY,mBAAA,IAAsB,IAAI;AACxG,QAAM,EAAC,YAAA,IAAe,qBAAA;AACtB,QAAM,WAAW,YAAA;AACjB,QAAM,cAAc,cAAc,MAAM,iBAAA,CAAkB;AAC1D,SAAO,SAAS,gBAAgB,QAC1B,WAAW,aAAa,SAAS,QAAQ,SAAS,IAClD,iBAAiB,aAAa,SAAS,QAAQ;AACzD;AAYA,SAAwB,UAAU,OAAO;AACrC,QAAM,EAAC,MAAM,WAAW,OAAO,gBAAgB,MAAM,YAAY,oBAAoB,UAAU,IAAI,GAAG,aAAA,IAAgB,cAAc,OAAO,UAAU;AACrJ,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,aAAa,KAAA,IAAQ,qBAAA;AAC5B,QAAM,WAAW,YAAA;AACjB,QAAM,cAAc,cAAc,MAAM,iBAAA,CAAkB;AAC1D,QAAM,UAAU,SAAS,gBAAgB;AACzC,QAAM,YAAY,UAAU,WAAW,aAAa,SAAS,QAAQ,SAAS,IAAI,iBAAiB,aAAa,SAAS,QAAQ;AAEjI;AAAA;AAAA,IAEI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAc,OAAO,aAAa;AAAA,QAAO,SAAQ;AAAA,QAAa,0BAAwB;AAAA,QAAC,cAAY;AAAA,QACrF,OAAO,EAAC,UAAU,GAAG,GAAG,GAAA;AAAA,QAClC,UAAA,YAAY,IAAI,CAAC,QAAQ;AACtB,gBAAM,MAAM,UAAU,IAAI,QAAQ,IAAI;AACtC,gBAAM,OAAO,UAAU,GAAG,SAAS,QAAQ,GAAG,cAAc,SAAS,QAAQ,WAAW,IAAI,KAAK,CAAC,KAAK,IAAI;AAC3G,gBAAM,QAAQ,IAAI,UAAU,UAAa,IAAI,UAAU,OAAO,IAAI,QAC9D,qBAAC,SAAM,WAAU,QAAO,WAAU,OAAM,IAAI,EAAC,YAAY,UAAU,KAAK,KAAA,GACpE,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAM,cAAI,MAAA,CAAM;AAAA,YACjB,oBAAC,eAAA,EAAc,MAAK,SAAQ,OAAO,IAAI,OAAO,OAAO,EAAC,QAAQ,IAAI,UAAU,IAAI,YAAY,MAAG,CAAG;AAAA,UAAA,GACtG;AAEJ,iBACI;AAAA,YAAC;AAAA,YAAA;AAAA,cAAmB,OAAO;AAAA,cAAK;AAAA,cAAc,MAAM,IAAI;AAAA,cAAM,cAAa;AAAA,cAAQ,UAAU,IAAI;AAAA,cACvF,WAAW;AAAA,cAAM,IAAI;AAAA,cAAM,SAAS,MAAM,WAAW,GAAG;AAAA,cAAG,OAAO,EAAC,WAAW,GAAA;AAAA,YAAE;AAAA,YAD3E;AAAA,UAAA;AAAA,QAGvB,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA;AAGb;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function activeRouteTabOf(tabs, pathname) {
|
|
2
|
+
const path = pathname.replace(/\/+$/, "") || "/";
|
|
3
|
+
const matching = tabs.filter(({ to, exact }) => {
|
|
4
|
+
const tabPath = to.replace(/\/+$/, "") || "/";
|
|
5
|
+
return path === tabPath || !exact && path.startsWith(`${tabPath === "/" ? "" : tabPath}/`);
|
|
6
|
+
});
|
|
7
|
+
return matching.sort((first, second) => second.to.length - first.to.length)[0]?.to ?? null;
|
|
8
|
+
}
|
|
9
|
+
function queryTabOf(tabs, search, paramName) {
|
|
10
|
+
const wanted = new URLSearchParams(search).get(paramName);
|
|
11
|
+
return tabs.find((tab) => tab.value === wanted)?.value ?? tabs[0]?.value ?? null;
|
|
12
|
+
}
|
|
13
|
+
function searchWithTab(search, paramName, value) {
|
|
14
|
+
const params = new URLSearchParams(search);
|
|
15
|
+
params.set(paramName, value);
|
|
16
|
+
return `?${params.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
activeRouteTabOf,
|
|
20
|
+
queryTabOf,
|
|
21
|
+
searchWithTab
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=routeTabMatching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routeTabMatching.js","sources":["../../../src/ui/navigation/routeTabMatching.js"],"sourcesContent":["// Qué pestaña va activa según la dirección, para las pestañas por ruta (RouteTabs):\n// por ruta cada pestaña es una página: gana la que coincide con la ruta, o la más larga de la que es parte\n// ('/clientes/123/pagos/abril' activa '/clientes/123/pagos'); exact solo con la ruta idéntica\n// por consulta pestañas dentro de una misma pantalla, en ?tab=pagos: se puede compartir la liga a la pestaña\n//\n// Puro (sin React): se prueba con node (routeTabMatching.test.js).\n//\n// Genérico del paquete: function (regla de la casa).\n\n/**\n * @param {Array<{to: string, exact?: boolean}>} tabs\n * @param {string} pathname\n * @returns {string|null} el `to` de la pestaña activa\n */\nexport function activeRouteTabOf(tabs, pathname) {\n const path = pathname.replace(/\\/+$/, '') || '/';\n const matching = tabs.filter(({to, exact}) => {\n const tabPath = to.replace(/\\/+$/, '') || '/';\n return path === tabPath || (!exact && path.startsWith(`${tabPath === '/' ? '' : tabPath}/`));\n });\n // la más específica: la de la ruta más larga\n return matching.sort((first, second) => second.to.length - first.to.length)[0]?.to ?? null;\n}\n\n/**\n * @param {Array<{value: string}>} tabs\n * @param {string} search la consulta de la dirección ('?tab=pagos&page=2')\n * @param {string} paramName 'tab'\n * @returns {string|null} la pestaña de la consulta si existe; si no, la primera\n */\nexport function queryTabOf(tabs, search, paramName) {\n const wanted = new URLSearchParams(search).get(paramName);\n return tabs.find((tab) => tab.value === wanted)?.value ?? tabs[0]?.value ?? null;\n}\n\n/**\n * La consulta con otra pestaña, sin perder lo demás que traía (?page=2 se queda).\n *\n * @param {string} search\n * @param {string} paramName\n * @param {string} value\n * @returns {string} '?tab=pagos&page=2'\n */\nexport function searchWithTab(search, paramName, value) {\n const params = new URLSearchParams(search);\n params.set(paramName, value);\n return `?${params.toString()}`;\n}\n"],"names":[],"mappings":"AAcO,SAAS,iBAAiB,MAAM,UAAU;AAC7C,QAAM,OAAO,SAAS,QAAQ,QAAQ,EAAE,KAAK;AAC7C,QAAM,WAAW,KAAK,OAAO,CAAC,EAAC,IAAI,MAAK,MAAM;AAC1C,UAAM,UAAU,GAAG,QAAQ,QAAQ,EAAE,KAAK;AAC1C,WAAO,SAAS,WAAY,CAAC,SAAS,KAAK,WAAW,GAAG,YAAY,MAAM,KAAK,OAAO,GAAG;AAAA,EAC9F,CAAC;AAED,SAAO,SAAS,KAAK,CAAC,OAAO,WAAW,OAAO,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,MAAM;AAC1F;AAQO,SAAS,WAAW,MAAM,QAAQ,WAAW;AAChD,QAAM,SAAS,IAAI,gBAAgB,MAAM,EAAE,IAAI,SAAS;AACxD,SAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,UAAU,MAAM,GAAG,SAAS,KAAK,CAAC,GAAG,SAAS;AAChF;AAUO,SAAS,cAAc,QAAQ,WAAW,OAAO;AACpD,QAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,SAAO,IAAI,WAAW,KAAK;AAC3B,SAAO,IAAI,OAAO,SAAQ,CAAE;AAChC;"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useImperativeHandle } from "react";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Button from "@mui/material/Button";
|
|
6
|
+
import Chip from "@mui/material/Chip";
|
|
7
|
+
import FormHelperText from "@mui/material/FormHelperText";
|
|
8
|
+
import Stack from "@mui/material/Stack";
|
|
9
|
+
import Typography from "@mui/material/Typography";
|
|
10
|
+
import AttachFileRoundedIcon from "@mui/icons-material/AttachFileRounded";
|
|
11
|
+
import { translateValidationMessage } from "../forms/validationMessages.js";
|
|
12
|
+
import FieldFrame from "../inputs/FieldFrame.js";
|
|
13
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
14
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
15
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
16
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
17
|
+
import FileChip from "./FileChip.js";
|
|
18
|
+
import TemplateDownloadButton from "./TemplateDownloadButton.js";
|
|
19
|
+
import { useFilePicker } from "./useFilePicker.js";
|
|
20
|
+
const THEME_NAME = "CalcitaAttachmentsInput";
|
|
21
|
+
const AttachmentsRoot = piecePart(THEME_NAME, "Root", Box);
|
|
22
|
+
const AttachmentsActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
23
|
+
const AttachmentsButton = piecePart(THEME_NAME, "Button", Button);
|
|
24
|
+
const AttachmentsList = piecePart(THEME_NAME, "List", Stack);
|
|
25
|
+
const AttachmentsChip = piecePart(THEME_NAME, "FileChip", Chip);
|
|
26
|
+
const AttachmentsLimits = piecePart(THEME_NAME, "Limits", Typography);
|
|
27
|
+
function AttachmentsInput(props) {
|
|
28
|
+
const {
|
|
29
|
+
name,
|
|
30
|
+
label,
|
|
31
|
+
value = [],
|
|
32
|
+
onChange,
|
|
33
|
+
onBlur,
|
|
34
|
+
onRemove,
|
|
35
|
+
accept,
|
|
36
|
+
maxSize,
|
|
37
|
+
maxFiles,
|
|
38
|
+
maxTotalSize,
|
|
39
|
+
error,
|
|
40
|
+
helperText,
|
|
41
|
+
buttonLabel,
|
|
42
|
+
showButton = true,
|
|
43
|
+
pickerRef,
|
|
44
|
+
disabled = false,
|
|
45
|
+
busy = false,
|
|
46
|
+
onReject,
|
|
47
|
+
template,
|
|
48
|
+
actions,
|
|
49
|
+
sx,
|
|
50
|
+
texts,
|
|
51
|
+
...unknownProps
|
|
52
|
+
} = usePieceProps(props, THEME_NAME);
|
|
53
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
54
|
+
const { t } = useToolkitTranslation("common", texts);
|
|
55
|
+
const { formatFileSize } = useFormatters();
|
|
56
|
+
const generatedId = useId();
|
|
57
|
+
const buttonId = `${name ?? "attachments"}-${generatedId}`;
|
|
58
|
+
const errorText = translateValidationMessage(t, error);
|
|
59
|
+
const isLocked = disabled || busy;
|
|
60
|
+
const change = (files) => {
|
|
61
|
+
onChange(files);
|
|
62
|
+
onBlur?.();
|
|
63
|
+
};
|
|
64
|
+
const picker = useFilePicker({
|
|
65
|
+
accept,
|
|
66
|
+
maxSize,
|
|
67
|
+
multiple: true,
|
|
68
|
+
maxFiles,
|
|
69
|
+
maxTotalSize,
|
|
70
|
+
keptFiles: value,
|
|
71
|
+
disabled: isLocked,
|
|
72
|
+
onAccept: change,
|
|
73
|
+
onReject,
|
|
74
|
+
texts
|
|
75
|
+
});
|
|
76
|
+
useImperativeHandle(pickerRef, () => ({ open: picker.open }), [picker.open]);
|
|
77
|
+
const remove = (file) => {
|
|
78
|
+
onRemove?.(file);
|
|
79
|
+
change(value.filter((item) => item !== file));
|
|
80
|
+
};
|
|
81
|
+
const totalBytes = value.reduce((sum, file) => sum + (file.size ?? 0), 0);
|
|
82
|
+
const limitsText = maxFiles || picker.maxTotalBytes ? t(picker.maxTotalBytes ? "upload.summaryOf" : "upload.summary", {
|
|
83
|
+
count: value.length,
|
|
84
|
+
total: formatFileSize(totalBytes),
|
|
85
|
+
max: picker.maxTotalBytes ? formatFileSize(picker.maxTotalBytes) : void 0
|
|
86
|
+
}) : null;
|
|
87
|
+
const bottomText = errorText ?? helperText;
|
|
88
|
+
const content = /* @__PURE__ */ jsxs(
|
|
89
|
+
AttachmentsRoot,
|
|
90
|
+
{
|
|
91
|
+
"aria-busy": busy,
|
|
92
|
+
...picker.dropHandlers,
|
|
93
|
+
ownSx: {
|
|
94
|
+
borderRadius: 1,
|
|
95
|
+
outline: picker.isDragging ? 2 : 0,
|
|
96
|
+
outlineStyle: "dashed",
|
|
97
|
+
outlineColor: "primary.main",
|
|
98
|
+
bgcolor: picker.isDragging ? "action.hover" : "transparent",
|
|
99
|
+
...sx
|
|
100
|
+
},
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("input", { ...picker.inputProps, name, "aria-hidden": "true", tabIndex: -1 }),
|
|
103
|
+
(showButton || template || actions) && /* @__PURE__ */ jsxs(AttachmentsActions, { direction: "row", ownSx: { alignItems: "center", flexWrap: "wrap", gap: 1 }, children: [
|
|
104
|
+
showButton && /* @__PURE__ */ jsx(AttachmentsButton, { id: buttonId, size: "small", startIcon: /* @__PURE__ */ jsx(AttachFileRoundedIcon, {}), onClick: picker.open, disabled: isLocked, children: buttonLabel ?? t("upload.attach") }),
|
|
105
|
+
template && /* @__PURE__ */ jsx(TemplateDownloadButton, { size: "small", texts, ...template }),
|
|
106
|
+
actions
|
|
107
|
+
] }),
|
|
108
|
+
value.length > 0 && /* @__PURE__ */ jsx(AttachmentsList, { direction: "row", role: "list", ownSx: { flexWrap: "wrap", gap: 1, mt: showButton ? 1 : 0 }, children: value.map((file) => /* @__PURE__ */ jsx(Box, { role: "listitem", sx: { maxWidth: "100%" }, children: /* @__PURE__ */ jsx(
|
|
109
|
+
FileChip,
|
|
110
|
+
{
|
|
111
|
+
file,
|
|
112
|
+
ChipComponent: AttachmentsChip,
|
|
113
|
+
removeLabel: t("upload.remove", { name: file.name }),
|
|
114
|
+
onRemove: isLocked ? void 0 : () => remove(file)
|
|
115
|
+
}
|
|
116
|
+
) }, file.id ?? file.name)) }),
|
|
117
|
+
limitsText && /* @__PURE__ */ jsx(AttachmentsLimits, { variant: "caption", color: "text.secondary", ownSx: { display: "block", mt: 0.5 }, children: limitsText }),
|
|
118
|
+
bottomText && /* @__PURE__ */ jsx(FormHelperText, { error: Boolean(errorText), children: bottomText })
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return label ? /* @__PURE__ */ jsx(FieldFrame, { inputId: buttonId, label, children: content }) : content;
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
AttachmentsInput as default
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=AttachmentsInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttachmentsInput.js","sources":["../../src/upload/AttachmentsInput.jsx"],"sourcesContent":["// Varios archivos como en un correo: el botón «Adjuntar» con su clip y cada archivo en un chip con su × (Excel en verde,\n// PDF en rojo…). También se pueden soltar encima. Volver a elegir un archivo con el mismo nombre lo reemplaza (se corrigió en\n// disco) y no cuenta doble.\n//\n// Solo ELIGE (controlado: value / onChange recibe la lista completa), con las reglas de los demás cargadores (accept,\n// maxSize por archivo, maxFiles y maxTotalSize; useFilePicker). La lista puede mezclar archivos nuevos (File) con los que ya\n// están guardados ({id?, name, size?}): al editar un registro, los de antes se ven igual y también se quitan.\n//\n// Huecos: showButton={false} + pickerRef para abrir la ventana desde un botón suyo (el clip de la barra de un correo);\n// template («Descargar la plantilla»); actions (botones suyos junto a «Adjuntar»); onRemove (avisar al API que se quitó uno\n// que ya estaba guardado); label, buttonLabel y texts. Con formik: FormAttachmentsField; con react-hook-form, el del mismo\n// nombre.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaAttachmentsInput con defaultProps y\n// styleOverrides de sus partes: root, actions, button, list, fileChip, limits.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId, useImperativeHandle} from 'react';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport FormHelperText from '@mui/material/FormHelperText';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport FileChip from '@/upload/FileChip.jsx';\nimport TemplateDownloadButton from '@/upload/TemplateDownloadButton.jsx';\nimport {useFilePicker} from '@/upload/useFilePicker.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaAttachmentsInput)\nconst THEME_NAME = 'CalcitaAttachmentsInput';\nconst AttachmentsRoot = piecePart(THEME_NAME, 'Root', Box);\nconst AttachmentsActions = piecePart(THEME_NAME, 'Actions', Stack);\nconst AttachmentsButton = piecePart(THEME_NAME, 'Button', Button);\nconst AttachmentsList = piecePart(THEME_NAME, 'List', Stack);\nconst AttachmentsChip = piecePart(THEME_NAME, 'FileChip', Chip);\nconst AttachmentsLimits = piecePart(THEME_NAME, 'Limits', Typography);\n\n/**\n * La lista conserva el tipo que usa la pantalla para lo ya guardado (T): onChange regresa esos mismos y los File nuevos.\n *\n * @template {File|{id?: string, name: string, size?: number}} T\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida; sin ella, va sin etiqueta (dentro de un correo, por ejemplo)\n * @param {T[]} [props.value] los nuevos y los que ya están guardados\n * @param {(files: Array<T|File>) => void} props.onChange la lista completa\n * @param {() => void} [props.onBlur] se llama al agregar o quitar (para que el formulario lo marque como tocado)\n * @param {(file: T) => void} [props.onRemove] al quitar uno (antes de onChange)\n * @param {string|string[]} [props.accept] 'excel' | ['pdf', 'image'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [props.maxSize] el peso máximo de cada archivo: bytes o '10 MB'\n * @param {number} [props.maxFiles] cuántos como máximo\n * @param {number|string} [props.maxTotalSize] cuánto pueden pesar todos juntos\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] ya traducida\n * @param {string} [props.buttonLabel] ya traducido; por omisión «Adjuntar»\n * @param {boolean} [props.showButton] false esconde «Adjuntar» (la ventana se abre con pickerRef desde un botón suyo)\n * @param {import('react').Ref<{open: () => void}>} [props.pickerRef] recibe {open} para abrir la ventana de archivos\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.busy] candado mientras se sube: no se puede agregar ni quitar\n * @param {(file: File, reason: string) => void} [props.onReject] reason es uno de FILE_REJECTION; sin él, un aviso\n * @param {Parameters<typeof import('@/upload/TemplateDownloadButton.jsx').default>[0]} [props.template] «Descargar la\n * plantilla» junto a «Adjuntar»\n * @param {import('react').ReactNode} [props.actions] botones suyos junto a «Adjuntar»\n * @param {object} [props.sx]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function AttachmentsInput(props) {\n const {\n name, label, value = [], onChange, onBlur, onRemove, accept, maxSize, maxFiles, maxTotalSize, error, helperText, buttonLabel,\n showButton = true, pickerRef, disabled = false, busy = false, onReject, template, actions, sx, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const buttonId = `${name ?? 'attachments'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n const isLocked = disabled || busy;\n const change = (files) => {\n onChange(files);\n onBlur?.();\n };\n const picker = useFilePicker({\n accept, maxSize, multiple: true, maxFiles, maxTotalSize, keptFiles: value, disabled: isLocked, onAccept: change, onReject, texts,\n });\n useImperativeHandle(pickerRef, () => ({open: picker.open}), [picker.open]);\n\n const remove = (file) => {\n onRemove?.(file);\n change(value.filter((item) => item !== file));\n };\n const totalBytes = value.reduce((sum, file) => sum + (file.size ?? 0), 0);\n // cuántos y cuánto pesan, solo si hay algún tope que cuidar\n const limitsText = maxFiles || picker.maxTotalBytes\n ? t(picker.maxTotalBytes ? 'upload.summaryOf' : 'upload.summary', {\n count: value.length, total: formatFileSize(totalBytes), max: picker.maxTotalBytes ? formatFileSize(picker.maxTotalBytes) : undefined,\n })\n : null;\n const bottomText = errorText ?? helperText;\n\n const content = (\n <AttachmentsRoot aria-busy={busy} {...picker.dropHandlers}\n ownSx={{\n borderRadius: 1, outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main',\n bgcolor: picker.isDragging ? 'action.hover' : 'transparent', ...sx,\n }}>\n <input {...picker.inputProps} name={name} aria-hidden=\"true\" tabIndex={-1} />\n {(showButton || template || actions) && (\n <AttachmentsActions direction=\"row\" ownSx={{alignItems: 'center', flexWrap: 'wrap', gap: 1}}>\n {showButton && (\n <AttachmentsButton id={buttonId} size=\"small\" startIcon={<AttachFileRoundedIcon />} onClick={picker.open} disabled={isLocked}>\n {buttonLabel ?? t('upload.attach')}\n </AttachmentsButton>\n )}\n {template && <TemplateDownloadButton size=\"small\" texts={texts} {...template} />}\n {actions}\n </AttachmentsActions>\n )}\n {value.length > 0 && (\n <AttachmentsList direction=\"row\" role=\"list\" ownSx={{flexWrap: 'wrap', gap: 1, mt: showButton ? 1 : 0}}>\n {value.map((file) => (\n <Box key={file.id ?? file.name} role=\"listitem\" sx={{maxWidth: '100%'}}>\n <FileChip file={file} ChipComponent={AttachmentsChip} removeLabel={t('upload.remove', {name: file.name})}\n onRemove={isLocked ? undefined : () => remove(file)} />\n </Box>\n ))}\n </AttachmentsList>\n )}\n {limitsText && (\n <AttachmentsLimits variant=\"caption\" color=\"text.secondary\" ownSx={{display: 'block', mt: 0.5}}>{limitsText}</AttachmentsLimits>\n )}\n {bottomText && <FormHelperText error={Boolean(errorText)}>{bottomText}</FormHelperText>}\n </AttachmentsRoot>\n );\n\n return label ? <FieldFrame inputId={buttonId} label={label}>{content}</FieldFrame> : content;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import Chip from "@mui/material/Chip";
|
|
4
|
+
import CancelIcon from "@mui/icons-material/Cancel";
|
|
5
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
6
|
+
import { fileAppearanceOf } from "./fileAppearance.js";
|
|
7
|
+
function FileChip({ file, onRemove, removeLabel, ChipComponent = Chip, ownSx }) {
|
|
8
|
+
const { formatFileSize } = useFormatters();
|
|
9
|
+
const { Icon, color } = fileAppearanceOf(file.name);
|
|
10
|
+
const label = typeof file.size === "number" ? `${file.name} · ${formatFileSize(file.size)}` : file.name;
|
|
11
|
+
const chipSx = { maxWidth: "100%", height: "auto", py: 0.5, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" }, ...ownSx };
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
ChipComponent,
|
|
14
|
+
{
|
|
15
|
+
icon: /* @__PURE__ */ jsx(Icon, { fontSize: "small", color }),
|
|
16
|
+
label,
|
|
17
|
+
variant: "outlined",
|
|
18
|
+
onDelete: onRemove,
|
|
19
|
+
deleteIcon: /* @__PURE__ */ jsx(CancelIcon, { "aria-label": removeLabel }),
|
|
20
|
+
...ChipComponent === Chip ? { sx: chipSx } : { ownSx: chipSx }
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
FileChip as default
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=FileChip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileChip.js","sources":["../../src/upload/FileChip.jsx"],"sourcesContent":["// Un archivo elegido como chip: su ícono según el tipo, su nombre y su peso, y la × para quitarlo. Lo comparten los\n// cargadores del paquete; cada uno le pasa su parte del tema (ChipComponent) para que el proyecto la cambie desde ahí.\n//\n// Sirve igual para un archivo nuevo (File) y para uno que ya está guardado en el servidor ({name, size?}): sin peso, solo\n// se ve el nombre. Un nombre largo se parte en renglones en lugar de cortarse: es lo que la persona revisa antes de subir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Chip from '@mui/material/Chip';\nimport CancelIcon from '@mui/icons-material/Cancel';\n\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {fileAppearanceOf} from '@/upload/fileAppearance.js';\n\n/**\n * @param {object} props\n * @param {{name: string, size?: number}} props.file\n * @param {(() => void)|undefined} [props.onRemove] sin él no hay × (con el candado puesto, por ejemplo)\n * @param {string} props.removeLabel ya traducido: «Quitar contrato.pdf»\n * @param {import('react').ElementType} [props.ChipComponent] la parte del tema del cargador (por omisión, el Chip de MUI)\n * @param {object} [props.ownSx] el estilo de la casa, debajo del tema del proyecto\n */\nexport default function FileChip({file, onRemove, removeLabel, ChipComponent = Chip, ownSx}) {\n const {formatFileSize} = useFormatters();\n const {Icon, color} = fileAppearanceOf(file.name);\n const label = typeof file.size === 'number' ? `${file.name} · ${formatFileSize(file.size)}` : file.name;\n const chipSx = {maxWidth: '100%', height: 'auto', py: 0.5, '& .MuiChip-label': {whiteSpace: 'normal', overflowWrap: 'anywhere'}, ...ownSx};\n\n return (\n <ChipComponent\n icon={<Icon fontSize=\"small\" color={color} />}\n label={label}\n variant=\"outlined\"\n onDelete={onRemove}\n deleteIcon={<CancelIcon aria-label={removeLabel} />}\n {...(ChipComponent === Chip ? {sx: chipSx} : {ownSx: chipSx})}\n />\n );\n}\n"],"names":[],"mappings":";;;;;;AAqBA,SAAwB,SAAS,EAAC,MAAM,UAAU,aAAa,gBAAgB,MAAM,SAAQ;AACzF,QAAM,EAAC,eAAA,IAAkB,cAAA;AACzB,QAAM,EAAC,MAAM,MAAA,IAAS,iBAAiB,KAAK,IAAI;AAChD,QAAM,QAAQ,OAAO,KAAK,SAAS,WAAW,GAAG,KAAK,IAAI,MAAM,eAAe,KAAK,IAAI,CAAC,KAAK,KAAK;AACnG,QAAM,SAAS,EAAC,UAAU,QAAQ,QAAQ,QAAQ,IAAI,KAAK,oBAAoB,EAAC,YAAY,UAAU,cAAc,WAAA,GAAa,GAAG,MAAA;AAEpI,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,MAAM,oBAAC,MAAA,EAAK,UAAS,SAAQ,OAAc;AAAA,MAC3C;AAAA,MACA,SAAQ;AAAA,MACR,UAAU;AAAA,MACV,YAAY,oBAAC,YAAA,EAAW,cAAY,YAAA,CAAa;AAAA,MAChD,GAAI,kBAAkB,OAAO,EAAC,IAAI,OAAA,IAAU,EAAC,OAAO,OAAA;AAAA,IAAM;AAAA,EAAA;AAGvE;"}
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
4
|
-
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
5
3
|
import Box from "@mui/material/Box";
|
|
6
4
|
import Button from "@mui/material/Button";
|
|
7
5
|
import Chip from "@mui/material/Chip";
|
|
8
6
|
import Paper from "@mui/material/Paper";
|
|
9
7
|
import Stack from "@mui/material/Stack";
|
|
10
8
|
import Typography from "@mui/material/Typography";
|
|
11
|
-
import CancelIcon from "@mui/icons-material/Cancel";
|
|
12
|
-
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
|
13
|
-
import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined";
|
|
14
|
-
import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutlined";
|
|
15
|
-
import PictureAsPdfOutlinedIcon from "@mui/icons-material/PictureAsPdfOutlined";
|
|
16
|
-
import TableViewOutlinedIcon from "@mui/icons-material/TableViewOutlined";
|
|
17
9
|
import ModalWaiting from "../feedback/ModalWaiting.js";
|
|
18
|
-
import { toastError } from "../feedback/toast.js";
|
|
19
|
-
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
20
|
-
import { resolveAccept, parseFileSize, matchesAccept, FILE_REJECTION } from "./fileRules.js";
|
|
21
10
|
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
11
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
12
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
22
13
|
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
14
|
+
import FileChip from "./FileChip.js";
|
|
15
|
+
import TemplateDownloadButton from "./TemplateDownloadButton.js";
|
|
16
|
+
import { useFilePicker } from "./useFilePicker.js";
|
|
23
17
|
const THEME_NAME = "CalcitaFileDropzone";
|
|
24
18
|
const DropzoneRoot = piecePart(THEME_NAME, "Root", Paper);
|
|
25
19
|
const DropzoneFileArea = piecePart(THEME_NAME, "FileArea", Box);
|
|
@@ -28,13 +22,6 @@ const DropzoneHint = piecePart(THEME_NAME, "Hint", Typography);
|
|
|
28
22
|
const DropzoneActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
29
23
|
const DropzoneLimits = piecePart(THEME_NAME, "Limits", Typography);
|
|
30
24
|
const DropzoneExtra = piecePart(THEME_NAME, "Extra", Box);
|
|
31
|
-
const FILE_APPEARANCES = [
|
|
32
|
-
{ pattern: /\.(xlsx?|csv)$/, Icon: TableViewOutlinedIcon, color: "success" },
|
|
33
|
-
{ pattern: /\.pdf$/, Icon: PictureAsPdfOutlinedIcon, color: "error" },
|
|
34
|
-
{ pattern: /\.(png|jpe?g|gif|webp|svg|avif)$/, Icon: ImageOutlinedIcon, color: "info" },
|
|
35
|
-
{ pattern: /\.(docx?|txt|md)$/, Icon: DescriptionOutlinedIcon, color: "primary" }
|
|
36
|
-
];
|
|
37
|
-
const DEFAULT_FILE_APPEARANCE = { Icon: InsertDriveFileOutlinedIcon, color: "action" };
|
|
38
25
|
function FileDropzone(props) {
|
|
39
26
|
const {
|
|
40
27
|
value = null,
|
|
@@ -47,6 +34,7 @@ function FileDropzone(props) {
|
|
|
47
34
|
busy = false,
|
|
48
35
|
busyModal = false,
|
|
49
36
|
onReject,
|
|
37
|
+
template,
|
|
50
38
|
actions,
|
|
51
39
|
children,
|
|
52
40
|
sx,
|
|
@@ -56,96 +44,43 @@ function FileDropzone(props) {
|
|
|
56
44
|
warnUnknownProps(THEME_NAME, unknownProps);
|
|
57
45
|
const { t } = useToolkitTranslation("common", texts);
|
|
58
46
|
const { formatFileSize } = useFormatters();
|
|
59
|
-
const inputRef = useRef(null);
|
|
60
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
61
47
|
const isLocked = disabled || busy;
|
|
62
|
-
const resolvedAccept = resolveAccept(accept);
|
|
63
|
-
const maxBytes = parseFileSize(maxSize);
|
|
64
48
|
const hintText = hint ?? t("upload.hint");
|
|
65
|
-
const
|
|
66
|
-
if (onReject) {
|
|
67
|
-
onReject(file, reason);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const message = reason === FILE_REJECTION.size ? t("upload.tooLarge", { name: file.name, max: formatFileSize(maxBytes) }) : t("upload.invalidType", { name: file.name, accept: resolvedAccept });
|
|
71
|
-
toastError(message);
|
|
72
|
-
};
|
|
73
|
-
const pick = (file) => {
|
|
74
|
-
if (!file || isLocked) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (!matchesAccept(file, resolvedAccept)) {
|
|
78
|
-
reject(file, FILE_REJECTION.type);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (maxBytes && file.size > maxBytes) {
|
|
82
|
-
reject(file, FILE_REJECTION.size);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
onChange(file);
|
|
86
|
-
};
|
|
87
|
-
const openPicker = () => {
|
|
88
|
-
if (inputRef.current) {
|
|
89
|
-
inputRef.current.value = "";
|
|
90
|
-
}
|
|
91
|
-
inputRef.current?.click();
|
|
92
|
-
};
|
|
93
|
-
const appearance = value ? FILE_APPEARANCES.find(({ pattern }) => pattern.test(value.name.toLowerCase())) ?? DEFAULT_FILE_APPEARANCE : null;
|
|
49
|
+
const picker = useFilePicker({ accept, maxSize, disabled: isLocked, onAccept: onChange, onReject, texts });
|
|
94
50
|
return /* @__PURE__ */ jsxs(
|
|
95
51
|
DropzoneRoot,
|
|
96
52
|
{
|
|
97
53
|
variant: "outlined",
|
|
98
54
|
"aria-busy": busy,
|
|
99
|
-
|
|
100
|
-
event.preventDefault();
|
|
101
|
-
setIsDragging(!isLocked);
|
|
102
|
-
},
|
|
103
|
-
onDragLeave: () => setIsDragging(false),
|
|
104
|
-
onDrop: (event) => {
|
|
105
|
-
event.preventDefault();
|
|
106
|
-
setIsDragging(false);
|
|
107
|
-
pick(event.dataTransfer.files?.[0]);
|
|
108
|
-
},
|
|
55
|
+
...picker.dropHandlers,
|
|
109
56
|
ownSx: {
|
|
110
57
|
p: { xs: 3, sm: 5 },
|
|
111
58
|
textAlign: "center",
|
|
112
59
|
borderStyle: "dashed",
|
|
113
60
|
borderWidth: 2,
|
|
114
|
-
borderColor: isDragging ? "primary.main" : "divider",
|
|
115
|
-
bgcolor: isDragging ? "action.hover" : "background.paper",
|
|
61
|
+
borderColor: picker.isDragging ? "primary.main" : "divider",
|
|
62
|
+
bgcolor: picker.isDragging ? "action.hover" : "background.paper",
|
|
116
63
|
transition: "border-color .15s ease, background-color .15s ease",
|
|
117
64
|
opacity: disabled ? 0.6 : 1,
|
|
118
65
|
...sx
|
|
119
66
|
},
|
|
120
67
|
children: [
|
|
121
|
-
/* @__PURE__ */ jsx(
|
|
122
|
-
"input",
|
|
123
|
-
{
|
|
124
|
-
ref: inputRef,
|
|
125
|
-
type: "file",
|
|
126
|
-
accept: resolvedAccept,
|
|
127
|
-
hidden: true,
|
|
128
|
-
disabled: isLocked,
|
|
129
|
-
"aria-label": hintText,
|
|
130
|
-
onChange: (event) => pick(event.target.files?.[0])
|
|
131
|
-
}
|
|
132
|
-
),
|
|
68
|
+
/* @__PURE__ */ jsx("input", { ...picker.inputProps, "aria-label": hintText }),
|
|
133
69
|
value ? /* @__PURE__ */ jsx(DropzoneFileArea, { ownSx: { mb: 2 }, children: /* @__PURE__ */ jsx(
|
|
134
|
-
|
|
70
|
+
FileChip,
|
|
135
71
|
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
deleteIcon: /* @__PURE__ */ jsx(CancelIcon, { "aria-label": t("upload.remove", { name: value.name }) }),
|
|
141
|
-
ownSx: { maxWidth: "100%", height: "auto", py: 0.5, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" } }
|
|
72
|
+
file: value,
|
|
73
|
+
ChipComponent: DropzoneFileChip,
|
|
74
|
+
removeLabel: t("upload.remove", { name: value.name }),
|
|
75
|
+
onRemove: isLocked ? void 0 : () => onChange(null)
|
|
142
76
|
}
|
|
143
77
|
) }) : /* @__PURE__ */ jsx(DropzoneHint, { variant: "body1", ownSx: { mb: 2, overflowWrap: "anywhere" }, children: hintText }),
|
|
144
78
|
/* @__PURE__ */ jsxs(DropzoneActions, { direction: { xs: "column", sm: "row" }, spacing: 2, ownSx: { justifyContent: "center", alignItems: { xs: "stretch", sm: "center" } }, children: [
|
|
145
|
-
/* @__PURE__ */ jsx(Button, { variant: "outlined", onClick:
|
|
79
|
+
/* @__PURE__ */ jsx(Button, { variant: "outlined", onClick: picker.open, disabled: isLocked, children: buttonLabel ?? t("upload.choose") }),
|
|
80
|
+
template && /* @__PURE__ */ jsx(TemplateDownloadButton, { texts, ...template }),
|
|
146
81
|
actions
|
|
147
82
|
] }),
|
|
148
|
-
maxBytes && /* @__PURE__ */ jsx(DropzoneLimits, { variant: "caption", color: "text.secondary", ownSx: { display: "block", mt: 1.5 }, children: t("upload.maxSizeHint", { max: formatFileSize(maxBytes) }) }),
|
|
83
|
+
picker.maxBytes && /* @__PURE__ */ jsx(DropzoneLimits, { variant: "caption", color: "text.secondary", ownSx: { display: "block", mt: 1.5 }, children: t("upload.maxSizeHint", { max: formatFileSize(picker.maxBytes) }) }),
|
|
149
84
|
children && /* @__PURE__ */ jsx(DropzoneExtra, { ownSx: { mt: 3, textAlign: "left" }, children }),
|
|
150
85
|
busyModal && /* @__PURE__ */ jsx(ModalWaiting, { open: busy, title: busyModal.title, text: busyModal.text })
|
|
151
86
|
]
|