@stackcraftmx/calcita 0.32.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/security/SessionsCard.js +11 -7
- package/dist/security/SessionsCard.js.map +1 -1
- 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,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState, useEffect } from "react";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Button from "@mui/material/Button";
|
|
6
|
+
import ButtonBase from "@mui/material/ButtonBase";
|
|
7
|
+
import FormHelperText from "@mui/material/FormHelperText";
|
|
8
|
+
import Stack from "@mui/material/Stack";
|
|
9
|
+
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
|
|
10
|
+
import { translateValidationMessage } from "../forms/validationMessages.js";
|
|
11
|
+
import FieldFrame from "../inputs/FieldFrame.js";
|
|
12
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
13
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
14
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
15
|
+
import { useFilePicker } from "./useFilePicker.js";
|
|
16
|
+
const IMAGE_SHAPE = Object.freeze({ circle: "circle", rounded: "rounded" });
|
|
17
|
+
const THEME_NAME = "CalcitaImageInput";
|
|
18
|
+
const ImageRoot = piecePart(THEME_NAME, "Root", Stack);
|
|
19
|
+
const ImagePreview = piecePart(THEME_NAME, "Preview", ButtonBase);
|
|
20
|
+
const ImagePicture = piecePart(THEME_NAME, "Image", "img");
|
|
21
|
+
const ImagePlaceholder = piecePart(THEME_NAME, "Placeholder", Box);
|
|
22
|
+
const ImageActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
23
|
+
const DEFAULT_PREVIEW_SIZE = 96;
|
|
24
|
+
function usePreviewUrl(value) {
|
|
25
|
+
const [fileUrl, setFileUrl] = useState(null);
|
|
26
|
+
const isFile = typeof File !== "undefined" && value instanceof File;
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isFile) {
|
|
29
|
+
setFileUrl(null);
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const url = URL.createObjectURL(value);
|
|
33
|
+
setFileUrl(url);
|
|
34
|
+
return () => URL.revokeObjectURL(url);
|
|
35
|
+
}, [isFile, value]);
|
|
36
|
+
if (isFile) {
|
|
37
|
+
return fileUrl;
|
|
38
|
+
}
|
|
39
|
+
return typeof value === "string" && value ? value : null;
|
|
40
|
+
}
|
|
41
|
+
function ImageInput(props) {
|
|
42
|
+
const {
|
|
43
|
+
name,
|
|
44
|
+
label,
|
|
45
|
+
value = null,
|
|
46
|
+
onChange,
|
|
47
|
+
onBlur,
|
|
48
|
+
shape = IMAGE_SHAPE.circle,
|
|
49
|
+
size = DEFAULT_PREVIEW_SIZE,
|
|
50
|
+
alt,
|
|
51
|
+
placeholder,
|
|
52
|
+
accept = "image",
|
|
53
|
+
maxSize,
|
|
54
|
+
error,
|
|
55
|
+
helperText = " ",
|
|
56
|
+
chooseLabel,
|
|
57
|
+
removeLabel,
|
|
58
|
+
disabled = false,
|
|
59
|
+
busy = false,
|
|
60
|
+
onReject,
|
|
61
|
+
sx,
|
|
62
|
+
texts,
|
|
63
|
+
...unknownProps
|
|
64
|
+
} = usePieceProps(props, THEME_NAME);
|
|
65
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
66
|
+
const { t } = useToolkitTranslation("common", texts);
|
|
67
|
+
const generatedId = useId();
|
|
68
|
+
const previewId = `${name ?? "image"}-${generatedId}`;
|
|
69
|
+
const errorText = translateValidationMessage(t, error);
|
|
70
|
+
const isLocked = disabled || busy;
|
|
71
|
+
const previewUrl = usePreviewUrl(value);
|
|
72
|
+
const change = (file) => {
|
|
73
|
+
onChange(file);
|
|
74
|
+
onBlur?.();
|
|
75
|
+
};
|
|
76
|
+
const picker = useFilePicker({ accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts });
|
|
77
|
+
const pickText = chooseLabel ?? t(value ? "upload.changeImage" : "upload.chooseImage");
|
|
78
|
+
return /* @__PURE__ */ jsx(FieldFrame, { inputId: previewId, label, children: /* @__PURE__ */ jsxs("div", { children: [
|
|
79
|
+
/* @__PURE__ */ jsxs(ImageRoot, { direction: "row", "aria-busy": busy, ownSx: { alignItems: "center", gap: 2, ...sx }, children: [
|
|
80
|
+
/* @__PURE__ */ jsx("input", { ...picker.inputProps, name, "aria-hidden": "true", tabIndex: -1 }),
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
ImagePreview,
|
|
83
|
+
{
|
|
84
|
+
id: previewId,
|
|
85
|
+
onClick: picker.open,
|
|
86
|
+
disabled: isLocked,
|
|
87
|
+
"aria-label": pickText,
|
|
88
|
+
...picker.dropHandlers,
|
|
89
|
+
ownSx: {
|
|
90
|
+
width: size,
|
|
91
|
+
height: size,
|
|
92
|
+
flexShrink: 0,
|
|
93
|
+
overflow: "hidden",
|
|
94
|
+
borderRadius: shape === IMAGE_SHAPE.circle ? "50%" : 2,
|
|
95
|
+
bgcolor: "action.hover",
|
|
96
|
+
color: "text.secondary",
|
|
97
|
+
outline: picker.isDragging ? 2 : 0,
|
|
98
|
+
outlineStyle: "dashed",
|
|
99
|
+
outlineColor: "primary.main",
|
|
100
|
+
outlineOffset: 2,
|
|
101
|
+
"&.Mui-focusVisible": { outline: 2, outlineStyle: "solid", outlineColor: "primary.main", outlineOffset: 2 }
|
|
102
|
+
},
|
|
103
|
+
children: previewUrl ? /* @__PURE__ */ jsx(
|
|
104
|
+
ImagePicture,
|
|
105
|
+
{
|
|
106
|
+
src: previewUrl,
|
|
107
|
+
alt: alt ?? (typeof label === "string" ? label : ""),
|
|
108
|
+
ownSx: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
|
|
109
|
+
}
|
|
110
|
+
) : /* @__PURE__ */ jsx(ImagePlaceholder, { ownSx: { display: "grid", placeItems: "center", width: "100%", height: "100%" }, children: placeholder ?? /* @__PURE__ */ jsx(AddPhotoAlternateOutlinedIcon, {}) })
|
|
111
|
+
}
|
|
112
|
+
),
|
|
113
|
+
/* @__PURE__ */ jsxs(ImageActions, { ownSx: { alignItems: "flex-start", gap: 0.5 }, children: [
|
|
114
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "outlined", onClick: picker.open, disabled: isLocked, children: pickText }),
|
|
115
|
+
value && /* @__PURE__ */ jsx(Button, { size: "small", color: "error", onClick: () => change(null), disabled: isLocked, children: removeLabel ?? t("upload.removeImage") })
|
|
116
|
+
] })
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsx(FormHelperText, { error: Boolean(errorText), children: errorText ?? helperText })
|
|
119
|
+
] }) });
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
IMAGE_SHAPE,
|
|
123
|
+
ImageInput as default
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=ImageInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageInput.js","sources":["../../src/upload/ImageInput.jsx"],"sourcesContent":["// Una imagen con su vista previa: la foto de un perfil (círculo) o la de un producto o una sucursal (recuadro). Se pica la\n// vista previa o «Elegir imagen» para abrir la ventana de archivos, o se suelta la imagen encima; «Quitar la imagen» la\n// borra. Dentro de una FormSection se vuelve renglón, como los demás campos.\n//\n// Solo ELIGE (controlado), con las reglas de los demás cargadores (accept, por omisión imágenes; maxSize; useFilePicker).\n// value es la imagen nueva (File), la que ya está guardada (su dirección, en texto) o null; onChange recibe File o null.\n// Enviarla es de la pantalla.\n//\n// La vista previa de un archivo nuevo se pinta en el navegador, sin subirlo (URL.createObjectURL); se libera al cambiarla.\n//\n// Huecos: shape (IMAGE_SHAPE), size, placeholder (lo que se ve sin imagen: las iniciales de la persona, por ejemplo), alt,\n// los textos de los botones y texts. Con formik: FormImageField; con react-hook-form, el del mismo nombre.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaImageInput con defaultProps y\n// styleOverrides de sus partes: root, preview, image, placeholder, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useId, useState} from 'react';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport FormHelperText from '@mui/material/FormHelperText';\nimport Stack from '@mui/material/Stack';\nimport AddPhotoAlternateOutlinedIcon from '@mui/icons-material/AddPhotoAlternateOutlined';\n\nimport {translateValidationMessage} from '@/forms/validationMessages.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {useFilePicker} from '@/upload/useFilePicker.js';\n\n/** La forma de la vista previa: círculo para una persona, recuadro para lo demás. */\nexport const IMAGE_SHAPE = Object.freeze({circle: 'circle', rounded: 'rounded'});\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaImageInput)\nconst THEME_NAME = 'CalcitaImageInput';\nconst ImageRoot = piecePart(THEME_NAME, 'Root', Stack);\nconst ImagePreview = piecePart(THEME_NAME, 'Preview', ButtonBase);\nconst ImagePicture = piecePart(THEME_NAME, 'Image', 'img');\nconst ImagePlaceholder = piecePart(THEME_NAME, 'Placeholder', Box);\nconst ImageActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n// el tamaño de la vista previa si la pantalla no dice otro (como un avatar grande de perfil)\nconst DEFAULT_PREVIEW_SIZE = 96;\n\n/**\n * La dirección de la vista previa: la de un archivo nuevo se crea en el navegador y se libera al cambiar; la guardada se\n * usa tal cual.\n *\n * @param {File|string|null} value\n * @returns {string|null}\n */\nfunction usePreviewUrl(value) {\n const [fileUrl, setFileUrl] = useState(null);\n const isFile = typeof File !== 'undefined' && value instanceof File;\n\n useEffect(() => {\n if (!isFile) {\n setFileUrl(null);\n return undefined;\n }\n const url = URL.createObjectURL(value);\n setFileUrl(url);\n return () => URL.revokeObjectURL(url);\n }, [isFile, value]);\n\n if (isFile) {\n return fileUrl;\n }\n return typeof value === 'string' && value ? value : null;\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida\n * @param {File|string|null} [props.value] la imagen nueva, la dirección de la que ya está guardada o null\n * @param {(file: File|null) => void} props.onChange null al quitarla\n * @param {() => void} [props.onBlur] se llama al elegir o quitar (para que el formulario lo marque como tocado)\n * @param {'circle'|'rounded'} [props.shape] IMAGE_SHAPE; por omisión, círculo\n * @param {number} [props.size] el lado de la vista previa en px; por omisión 96\n * @param {string} [props.alt] ya traducido: qué se ve en la imagen (por omisión, la etiqueta)\n * @param {import('react').ReactNode} [props.placeholder] lo que se ve sin imagen; por omisión, un ícono\n * @param {string|string[]} [props.accept] por omisión, cualquier imagen\n * @param {number|string} [props.maxSize] bytes o '2 MB'\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] ya traducida (p. ej. «JPG o PNG, cuadrada, hasta 2 MB»)\n * @param {string} [props.chooseLabel] ya traducido; por omisión «Elegir imagen» (o «Cambiar la imagen» si ya hay una)\n * @param {string} [props.removeLabel] ya traducido; por omisión «Quitar la imagen»\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.busy] candado mientras se sube\n * @param {(file: File, reason: string) => void} [props.onReject] reason es uno de FILE_REJECTION; sin él, un aviso\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 ImageInput(props) {\n const {\n name, label, value = null, onChange, onBlur, shape = IMAGE_SHAPE.circle, size = DEFAULT_PREVIEW_SIZE, alt, placeholder,\n accept = 'image', maxSize, error, helperText = ' ', chooseLabel, removeLabel, disabled = false, busy = false, onReject, sx, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const generatedId = useId();\n const previewId = `${name ?? 'image'}-${generatedId}`;\n const errorText = translateValidationMessage(t, error);\n const isLocked = disabled || busy;\n const previewUrl = usePreviewUrl(value);\n const change = (file) => {\n onChange(file);\n onBlur?.();\n };\n const picker = useFilePicker({accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts});\n const pickText = chooseLabel ?? t(value ? 'upload.changeImage' : 'upload.chooseImage');\n\n return (\n <FieldFrame inputId={previewId} label={label}>\n <div>\n <ImageRoot direction=\"row\" aria-busy={busy} ownSx={{alignItems: 'center', gap: 2, ...sx}}>\n <input {...picker.inputProps} name={name} aria-hidden=\"true\" tabIndex={-1} />\n <ImagePreview\n id={previewId}\n onClick={picker.open}\n disabled={isLocked}\n aria-label={pickText}\n {...picker.dropHandlers}\n ownSx={{\n width: size, height: size, flexShrink: 0, overflow: 'hidden',\n borderRadius: shape === IMAGE_SHAPE.circle ? '50%' : 2,\n bgcolor: 'action.hover', color: 'text.secondary',\n outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main', outlineOffset: 2,\n '&.Mui-focusVisible': {outline: 2, outlineStyle: 'solid', outlineColor: 'primary.main', outlineOffset: 2},\n }}\n >\n {previewUrl ? (\n <ImagePicture src={previewUrl} alt={alt ?? (typeof label === 'string' ? label : '')}\n ownSx={{width: '100%', height: '100%', objectFit: 'cover', display: 'block'}} />\n ) : (\n <ImagePlaceholder ownSx={{display: 'grid', placeItems: 'center', width: '100%', height: '100%'}}>\n {placeholder ?? <AddPhotoAlternateOutlinedIcon />}\n </ImagePlaceholder>\n )}\n </ImagePreview>\n <ImageActions ownSx={{alignItems: 'flex-start', gap: 0.5}}>\n <Button size=\"small\" variant=\"outlined\" onClick={picker.open} disabled={isLocked}>{pickText}</Button>\n {value && (\n <Button size=\"small\" color=\"error\" onClick={() => change(null)} disabled={isLocked}>\n {removeLabel ?? t('upload.removeImage')}\n </Button>\n )}\n </ImageActions>\n </ImageRoot>\n <FormHelperText error={Boolean(errorText)}>{errorText ?? helperText}</FormHelperText>\n </div>\n </FieldFrame>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import Button from "@mui/material/Button";
|
|
4
|
+
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
|
5
|
+
import { startDownload } from "../downloads/downloadQueue.js";
|
|
6
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
7
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
8
|
+
import { warnUnknownProps } from "../toolkit/unknownProps.js";
|
|
9
|
+
const THEME_NAME = "CalcitaTemplateDownloadButton";
|
|
10
|
+
const TemplateButtonRoot = piecePart(THEME_NAME, "Root", Button);
|
|
11
|
+
function TemplateDownloadButton(props) {
|
|
12
|
+
const {
|
|
13
|
+
href,
|
|
14
|
+
fileName,
|
|
15
|
+
request,
|
|
16
|
+
format,
|
|
17
|
+
label,
|
|
18
|
+
onDownload,
|
|
19
|
+
variant = "text",
|
|
20
|
+
size,
|
|
21
|
+
disabled = false,
|
|
22
|
+
startIcon,
|
|
23
|
+
sx,
|
|
24
|
+
texts,
|
|
25
|
+
...unknownProps
|
|
26
|
+
} = usePieceProps(props, THEME_NAME);
|
|
27
|
+
warnUnknownProps(THEME_NAME, unknownProps);
|
|
28
|
+
const { t } = useToolkitTranslation("common", texts);
|
|
29
|
+
const text = label ?? t("upload.template");
|
|
30
|
+
const icon = startIcon ?? /* @__PURE__ */ jsx(FileDownloadOutlinedIcon, {});
|
|
31
|
+
if (href) {
|
|
32
|
+
return /* @__PURE__ */ jsx(
|
|
33
|
+
TemplateButtonRoot,
|
|
34
|
+
{
|
|
35
|
+
component: "a",
|
|
36
|
+
href,
|
|
37
|
+
download: fileName ?? true,
|
|
38
|
+
variant,
|
|
39
|
+
size,
|
|
40
|
+
disabled,
|
|
41
|
+
startIcon: icon,
|
|
42
|
+
onClick: onDownload,
|
|
43
|
+
sx,
|
|
44
|
+
children: text
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return /* @__PURE__ */ jsx(
|
|
49
|
+
TemplateButtonRoot,
|
|
50
|
+
{
|
|
51
|
+
variant,
|
|
52
|
+
size,
|
|
53
|
+
disabled: disabled || !request,
|
|
54
|
+
startIcon: icon,
|
|
55
|
+
sx,
|
|
56
|
+
onClick: () => {
|
|
57
|
+
onDownload?.();
|
|
58
|
+
startDownload({ label: text, format, request });
|
|
59
|
+
},
|
|
60
|
+
children: text
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
TemplateDownloadButton as default
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=TemplateDownloadButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateDownloadButton.js","sources":["../../src/upload/TemplateDownloadButton.jsx"],"sourcesContent":["// «Descargar la plantilla»: el botón que va junto a un cargador para bajar el formato que el archivo debe seguir (el Excel\n// con sus columnas, el CSV de ejemplo). Los cargadores del paquete lo ponen en su lugar con su prop `template`; también se\n// usa suelto.\n//\n// Dos formas de bajarlo:\n// href un archivo fijo (public/plantillas/inventario.xlsx): un enlace de descarga, sin pasar por el panel.\n// request lo arma o lo da el API: va por startDownload, así sale en el panel de descargas con su avance y, si el\n// servidor dice que no, su motivo.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaTemplateDownloadButton con defaultProps y\n// styleOverrides de su parte root.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Button from '@mui/material/Button';\nimport FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';\n\nimport {startDownload} from '@/downloads/downloadQueue.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\n\n// el nombre de la pieza en el tema y su parte (theme.components.CalcitaTemplateDownloadButton)\nconst THEME_NAME = 'CalcitaTemplateDownloadButton';\nconst TemplateButtonRoot = piecePart(THEME_NAME, 'Root', Button);\n\n/**\n * @param {object} props\n * @param {string} [props.href] un archivo fijo; se baja con un enlace\n * @param {string} [props.fileName] con href: con qué nombre se guarda (por omisión, el del archivo)\n * @param {(transferOptions: {signal: AbortSignal, onDownloadProgress: (progress: {loaded: number, total?: number|null}) => void}) => Promise<{blob: Blob, fileName: string}>} [props.request]\n * lo pide al API y va al panel de descargas (startDownload)\n * @param {string} [props.format] con request: el tipo de archivo, para su ícono en el panel (xlsx, csv…)\n * @param {string} [props.label] ya traducido; por omisión «Descargar la plantilla»\n * @param {() => void} [props.onDownload] al empezar la descarga (para medir cuántos la bajan, por ejemplo)\n * @param {'text'|'outlined'|'contained'} [props.variant]\n * @param {'small'|'medium'|'large'} [props.size]\n * @param {boolean} [props.disabled]\n * @param {import('react').ReactNode} [props.startIcon] por omisión, la flecha de descarga\n * @param {object} [props.sx]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common`\n */\nexport default function TemplateDownloadButton(props) {\n const {\n href, fileName, request, format, label, onDownload, variant = 'text', size, disabled = false, startIcon, sx, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n const text = label ?? t('upload.template');\n const icon = startIcon ?? <FileDownloadOutlinedIcon />;\n\n // un archivo fijo: el enlace del navegador (download con su nombre)\n if (href) {\n return (\n <TemplateButtonRoot component=\"a\" href={href} download={fileName ?? true} variant={variant} size={size} disabled={disabled} startIcon={icon}\n onClick={onDownload} sx={sx}>\n {text}\n </TemplateButtonRoot>\n );\n }\n\n return (\n <TemplateButtonRoot\n variant={variant}\n size={size}\n disabled={disabled || !request}\n startIcon={icon}\n sx={sx}\n onClick={() => {\n onDownload?.();\n startDownload({label: text, format, request});\n }}\n >\n {text}\n </TemplateButtonRoot>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAsBA,MAAM,aAAa;AACnB,MAAM,qBAAqB,UAAU,YAAY,QAAQ,MAAM;AAkB/D,SAAwB,uBAAuB,OAAO;AAClD,QAAM;AAAA,IACF;AAAA,IAAM;AAAA,IAAU;AAAA,IAAS;AAAA,IAAQ;AAAA,IAAO;AAAA,IAAY,UAAU;AAAA,IAAQ;AAAA,IAAM,WAAW;AAAA,IAAO;AAAA,IAAW;AAAA,IAAI;AAAA,IAC7G,GAAG;AAAA,EAAA,IACH,cAAc,OAAO,UAAU;AACnC,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,OAAO,SAAS,EAAE,iBAAiB;AACzC,QAAM,OAAO,aAAa,oBAAC,0BAAA,CAAA,CAAyB;AAGpD,MAAI,MAAM;AACN,WACI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAmB,WAAU;AAAA,QAAI;AAAA,QAAY,UAAU,YAAY;AAAA,QAAM;AAAA,QAAkB;AAAA,QAAY;AAAA,QAAoB,WAAW;AAAA,QACnH,SAAS;AAAA,QAAY;AAAA,QACpC,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGb;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MACG;AAAA,MACA;AAAA,MACA,UAAU,YAAY,CAAC;AAAA,MACvB,WAAW;AAAA,MACX;AAAA,MACA,SAAS,MAAM;AACX,qBAAA;AACA,sBAAc,EAAC,OAAO,MAAM,QAAQ,SAAQ;AAAA,MAChD;AAAA,MAEC,UAAA;AAAA,IAAA;AAAA,EAAA;AAGb;"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
|
3
|
+
import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined";
|
|
4
|
+
import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutlined";
|
|
5
|
+
import PictureAsPdfOutlinedIcon from "@mui/icons-material/PictureAsPdfOutlined";
|
|
6
|
+
import TableViewOutlinedIcon from "@mui/icons-material/TableViewOutlined";
|
|
7
|
+
const FILE_APPEARANCES = [
|
|
8
|
+
{ pattern: /\.(xlsx?|csv)$/, Icon: TableViewOutlinedIcon, color: "success" },
|
|
9
|
+
{ pattern: /\.pdf$/, Icon: PictureAsPdfOutlinedIcon, color: "error" },
|
|
10
|
+
{ pattern: /\.(png|jpe?g|gif|webp|svg|avif)$/, Icon: ImageOutlinedIcon, color: "info" },
|
|
11
|
+
{ pattern: /\.(docx?|txt|md)$/, Icon: DescriptionOutlinedIcon, color: "primary" }
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_FILE_APPEARANCE = { Icon: InsertDriveFileOutlinedIcon, color: "action" };
|
|
14
|
+
function fileAppearanceOf(fileName) {
|
|
15
|
+
const lowerCaseName = fileName.toLowerCase();
|
|
16
|
+
return FILE_APPEARANCES.find(({ pattern }) => pattern.test(lowerCaseName)) ?? DEFAULT_FILE_APPEARANCE;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
fileAppearanceOf
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=fileAppearance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAppearance.js","sources":["../../src/upload/fileAppearance.js"],"sourcesContent":["// El ícono y el color de un archivo según su tipo, con los mismos colores que el panel de descargas: Excel y CSV en verde,\n// PDF en rojo, imágenes en azul, documentos en el color del tema. Lo comparten los cargadores del paquete.\n//\n// Genérico del paquete: function (regla de la casa).\nimport DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';\nimport ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';\nimport InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';\nimport PictureAsPdfOutlinedIcon from '@mui/icons-material/PictureAsPdfOutlined';\nimport TableViewOutlinedIcon from '@mui/icons-material/TableViewOutlined';\n\nconst FILE_APPEARANCES = [\n {pattern: /\\.(xlsx?|csv)$/, Icon: TableViewOutlinedIcon, color: 'success'},\n {pattern: /\\.pdf$/, Icon: PictureAsPdfOutlinedIcon, color: 'error'},\n {pattern: /\\.(png|jpe?g|gif|webp|svg|avif)$/, Icon: ImageOutlinedIcon, color: 'info'},\n {pattern: /\\.(docx?|txt|md)$/, Icon: DescriptionOutlinedIcon, color: 'primary'},\n];\nconst DEFAULT_FILE_APPEARANCE = {Icon: InsertDriveFileOutlinedIcon, color: 'action'};\n\n/**\n * @param {string} fileName\n * @returns {{Icon: import('react').ElementType, color: string}}\n */\nexport function fileAppearanceOf(fileName) {\n const lowerCaseName = fileName.toLowerCase();\n return FILE_APPEARANCES.find(({pattern}) => pattern.test(lowerCaseName)) ?? DEFAULT_FILE_APPEARANCE;\n}\n"],"names":[],"mappings":";;;;;;AAUA,MAAM,mBAAmB;AAAA,EACrB,EAAC,SAAS,kBAAkB,MAAM,uBAAuB,OAAO,UAAS;AAAA,EACzE,EAAC,SAAS,UAAU,MAAM,0BAA0B,OAAO,QAAO;AAAA,EAClE,EAAC,SAAS,oCAAoC,MAAM,mBAAmB,OAAO,OAAM;AAAA,EACpF,EAAC,SAAS,qBAAqB,MAAM,yBAAyB,OAAO,UAAS;AAClF;AACA,MAAM,0BAA0B,EAAC,MAAM,6BAA6B,OAAO,SAAQ;AAM5E,SAAS,iBAAiB,UAAU;AACvC,QAAM,gBAAgB,SAAS,YAAW;AAC1C,SAAO,iBAAiB,KAAK,CAAC,EAAC,QAAO,MAAM,QAAQ,KAAK,aAAa,CAAC,KAAK;AAChF;"}
|
package/dist/upload/fileRules.js
CHANGED
|
@@ -52,11 +52,37 @@ function parseFileSize(size) {
|
|
|
52
52
|
}
|
|
53
53
|
return Math.round(Number(match[1]) * BYTES_BY_UNIT[(match[2] ?? "b").toLowerCase()]);
|
|
54
54
|
}
|
|
55
|
+
function screenFiles(pickedFiles, { resolvedAccept, maxBytes = null, maxFiles, maxTotalBytes = null, keptFiles = [] } = {}) {
|
|
56
|
+
const kept = keptFiles.filter((keptFile) => !pickedFiles.some((picked) => picked.name === keptFile.name));
|
|
57
|
+
const accepted = [];
|
|
58
|
+
const rejected = [];
|
|
59
|
+
let totalBytes = kept.reduce((sum, keptFile) => sum + (keptFile.size ?? 0), 0);
|
|
60
|
+
for (const file of pickedFiles) {
|
|
61
|
+
let reason = null;
|
|
62
|
+
if (!matchesAccept(file, resolvedAccept)) {
|
|
63
|
+
reason = FILE_REJECTION.type;
|
|
64
|
+
} else if (maxBytes && file.size > maxBytes) {
|
|
65
|
+
reason = FILE_REJECTION.size;
|
|
66
|
+
} else if (maxFiles && kept.length + accepted.length >= maxFiles) {
|
|
67
|
+
reason = FILE_REJECTION.count;
|
|
68
|
+
} else if (maxTotalBytes && totalBytes + (file.size ?? 0) > maxTotalBytes) {
|
|
69
|
+
reason = FILE_REJECTION.total;
|
|
70
|
+
}
|
|
71
|
+
if (reason) {
|
|
72
|
+
rejected.push({ file, reason });
|
|
73
|
+
} else {
|
|
74
|
+
accepted.push(file);
|
|
75
|
+
totalBytes += file.size ?? 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { files: [...kept, ...accepted], accepted, rejected };
|
|
79
|
+
}
|
|
55
80
|
export {
|
|
56
81
|
FILE_ACCEPT_PRESETS,
|
|
57
82
|
FILE_REJECTION,
|
|
58
83
|
matchesAccept,
|
|
59
84
|
parseFileSize,
|
|
60
|
-
resolveAccept
|
|
85
|
+
resolveAccept,
|
|
86
|
+
screenFiles
|
|
61
87
|
};
|
|
62
88
|
//# sourceMappingURL=fileRules.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileRules.js","sources":["../../src/upload/fileRules.js"],"sourcesContent":["// Las reglas de qué archivo se acepta: su tipo (accept) y su peso (maxSize). Las comparten las dos zonas para soltar\n// archivos del paquete, la de MUI (upload/FileDropzone.jsx) y la de FilePond (filepond/FileDropzone.jsx), para que se\n// configuren igual.\n//\n// accept un nombre del menú ('excel'), una lista (['excel', 'pdf']) o lo de siempre del input ('.xlsx,.xls',\n// 'image/*'); se pueden mezclar: ['excel', '.dat'].\n// maxSize bytes (número) o texto con unidades: '500 KB', '10mb', '1 GB'.\n//\n// OJO: los tamaños van en base 1000 (1 MB = 1,000,000 bytes), igual que formatFileSize y que el navegador en sus\n// descargas. En base 1024 un límite de «10 MB» se pintaría como «10.5 MB».\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (fileRules.test.js).\n\n/**\n * Por qué se rechazó un archivo (lo que recibe onReject de las dos zonas). La de MUI solo rechaza por tipo y por peso;\n * la de FilePond además por el peso de todos juntos, por cantidad y por contenido (validate).\n */\nexport const FILE_REJECTION = Object.freeze({type: 'type', size: 'size', total: 'total', count: 'count', content: 'content'});\n\n/** El menú de tipos comunes, para no memorizar extensiones. */\nexport const FILE_ACCEPT_PRESETS = Object.freeze({\n excel: '.xlsx,.xls',\n csv: '.csv',\n pdf: '.pdf',\n word: '.doc,.docx',\n text: '.txt',\n json: '.json',\n xml: '.xml',\n zip: '.zip,.rar,.7z',\n image: 'image/*',\n audio: 'audio/*',\n video: 'video/*',\n});\n\n// bytes de cada unidad que se puede escribir en maxSize\nconst BYTES_BY_UNIT = Object.freeze({b: 1, kb: 1e3, mb: 1e6, gb: 1e9});\nconst SIZE_TEXT = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?\\s*$/i;\nconst ACCEPT_SEPARATOR = ',';\nconst WILDCARD_SUFFIX = '/*';\n\n/**\n * El accept del paquete como lo entiende el input del navegador.\n *\n * @param {string|string[]|undefined} accept 'excel' | ['excel', 'pdf'] | '.xlsx,.xls' | 'image/*'\n * @returns {string|undefined} '.xlsx,.xls,.pdf'; undefined si se acepta todo\n */\nexport function resolveAccept(accept) {\n if (!accept) {\n return undefined;\n }\n const parts = Array.isArray(accept) ? accept : [accept];\n return parts.map((part) => FILE_ACCEPT_PRESETS[String(part).toLowerCase()] ?? part).join(ACCEPT_SEPARATOR);\n}\n\n/**\n * ¿El archivo es de un tipo permitido? Revisa la extensión (.xlsx), el tipo exacto (application/pdf) o la familia\n * (image/*). El input del navegador ya filtra al elegir, pero al soltar un archivo no filtra nada.\n *\n * @param {{name: string, type?: string}} file\n * @param {string|undefined} resolvedAccept lo que regresa resolveAccept\n * @returns {boolean}\n */\nexport function matchesAccept(file, resolvedAccept) {\n if (!resolvedAccept) {\n return true;\n }\n const fileName = file.name.toLowerCase();\n const fileType = (file.type ?? '').toLowerCase();\n return resolvedAccept\n .split(ACCEPT_SEPARATOR)\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean)\n .some((rule) => {\n if (rule.startsWith('.')) {\n return fileName.endsWith(rule);\n }\n if (rule.endsWith(WILDCARD_SUFFIX)) {\n return fileType.startsWith(rule.slice(0, -1));\n }\n return fileType === rule;\n });\n}\n\n/**\n * Un tamaño escrito por una persona en bytes: '10 MB' → 10000000.\n *\n * @param {number|string|undefined|null} size\n * @returns {number|null} null si no hay límite\n */\nexport function parseFileSize(size) {\n if (size === undefined || size === null || size === '') {\n return null;\n }\n if (typeof size === 'number') {\n return size;\n }\n const match = SIZE_TEXT.exec(size);\n // guard clause: un límite mal escrito es error de quien configura, no algo que se ignore callado\n if (!match) {\n throw new Error(`«${size}» no es un tamaño: se espera un número de bytes o algo como '10 MB'`);\n }\n return Math.round(Number(match[1]) * BYTES_BY_UNIT[(match[2] ?? 'b').toLowerCase()]);\n}\n"],"names":[],"mappings":"AAiBY,MAAC,iBAAiB,OAAO,OAAO,EAAC,MAAM,QAAQ,MAAM,QAAQ,OAAO,SAAS,OAAO,SAAS,SAAS,UAAS,CAAC;AAGhH,MAAC,sBAAsB,OAAO,OAAO;AAAA,EAC7C,OAAO;AAAA,EACP,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACX,CAAC;AAGD,MAAM,gBAAgB,OAAO,OAAO,EAAC,GAAG,GAAG,IAAI,KAAK,IAAI,KAAK,IAAI,IAAG,CAAC;AACrE,MAAM,YAAY;AAClB,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AAQjB,SAAS,cAAc,QAAQ;AAClC,MAAI,CAAC,QAAQ;AACT,WAAO;AAAA,EACX;AACA,QAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AACtD,SAAO,MAAM,IAAI,CAAC,SAAS,oBAAoB,OAAO,IAAI,EAAE,YAAW,CAAE,KAAK,IAAI,EAAE,KAAK,gBAAgB;AAC7G;AAUO,SAAS,cAAc,MAAM,gBAAgB;AAChD,MAAI,CAAC,gBAAgB;AACjB,WAAO;AAAA,EACX;AACA,QAAM,WAAW,KAAK,KAAK,YAAW;AACtC,QAAM,YAAY,KAAK,QAAQ,IAAI,YAAW;AAC9C,SAAO,eACF,MAAM,gBAAgB,EACtB,IAAI,CAAC,SAAS,KAAK,KAAI,EAAG,YAAW,CAAE,EACvC,OAAO,OAAO,EACd,KAAK,CAAC,SAAS;AACZ,QAAI,KAAK,WAAW,GAAG,GAAG;AACtB,aAAO,SAAS,SAAS,IAAI;AAAA,IACjC;AACA,QAAI,KAAK,SAAS,eAAe,GAAG;AAChC,aAAO,SAAS,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAAA,IAChD;AACA,WAAO,aAAa;AAAA,EACxB,CAAC;AACT;AAQO,SAAS,cAAc,MAAM;AAChC,MAAI,SAAS,UAAa,SAAS,QAAQ,SAAS,IAAI;AACpD,WAAO;AAAA,EACX;AACA,MAAI,OAAO,SAAS,UAAU;AAC1B,WAAO;AAAA,EACX;AACA,QAAM,QAAQ,UAAU,KAAK,IAAI;AAEjC,MAAI,CAAC,OAAO;AACR,UAAM,IAAI,MAAM,IAAI,IAAI,qEAAqE;AAAA,EACjG;AACA,SAAO,KAAK,MAAM,OAAO,MAAM,CAAC,CAAC,IAAI,eAAe,MAAM,CAAC,KAAK,KAAK,YAAW,CAAE,CAAC;AACvF;"}
|
|
1
|
+
{"version":3,"file":"fileRules.js","sources":["../../src/upload/fileRules.js"],"sourcesContent":["// Las reglas de qué archivo se acepta: su tipo (accept) y su peso (maxSize). Las comparten las dos zonas para soltar\n// archivos del paquete, la de MUI (upload/FileDropzone.jsx) y la de FilePond (filepond/FileDropzone.jsx), para que se\n// configuren igual.\n//\n// accept un nombre del menú ('excel'), una lista (['excel', 'pdf']) o lo de siempre del input ('.xlsx,.xls',\n// 'image/*'); se pueden mezclar: ['excel', '.dat'].\n// maxSize bytes (número) o texto con unidades: '500 KB', '10mb', '1 GB'.\n//\n// OJO: los tamaños van en base 1000 (1 MB = 1,000,000 bytes), igual que formatFileSize y que el navegador en sus\n// descargas. En base 1024 un límite de «10 MB» se pintaría como «10.5 MB».\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (fileRules.test.js).\n\n/**\n * Por qué se rechazó un archivo (lo que recibe onReject de las dos zonas). La de MUI solo rechaza por tipo y por peso;\n * la de FilePond además por el peso de todos juntos, por cantidad y por contenido (validate).\n */\nexport const FILE_REJECTION = Object.freeze({type: 'type', size: 'size', total: 'total', count: 'count', content: 'content'});\n\n/** El menú de tipos comunes, para no memorizar extensiones. */\nexport const FILE_ACCEPT_PRESETS = Object.freeze({\n excel: '.xlsx,.xls',\n csv: '.csv',\n pdf: '.pdf',\n word: '.doc,.docx',\n text: '.txt',\n json: '.json',\n xml: '.xml',\n zip: '.zip,.rar,.7z',\n image: 'image/*',\n audio: 'audio/*',\n video: 'video/*',\n});\n\n// bytes de cada unidad que se puede escribir en maxSize\nconst BYTES_BY_UNIT = Object.freeze({b: 1, kb: 1e3, mb: 1e6, gb: 1e9});\nconst SIZE_TEXT = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?\\s*$/i;\nconst ACCEPT_SEPARATOR = ',';\nconst WILDCARD_SUFFIX = '/*';\n\n/**\n * El accept del paquete como lo entiende el input del navegador.\n *\n * @param {string|string[]|undefined} accept 'excel' | ['excel', 'pdf'] | '.xlsx,.xls' | 'image/*'\n * @returns {string|undefined} '.xlsx,.xls,.pdf'; undefined si se acepta todo\n */\nexport function resolveAccept(accept) {\n if (!accept) {\n return undefined;\n }\n const parts = Array.isArray(accept) ? accept : [accept];\n return parts.map((part) => FILE_ACCEPT_PRESETS[String(part).toLowerCase()] ?? part).join(ACCEPT_SEPARATOR);\n}\n\n/**\n * ¿El archivo es de un tipo permitido? Revisa la extensión (.xlsx), el tipo exacto (application/pdf) o la familia\n * (image/*). El input del navegador ya filtra al elegir, pero al soltar un archivo no filtra nada.\n *\n * @param {{name: string, type?: string}} file\n * @param {string|undefined} resolvedAccept lo que regresa resolveAccept\n * @returns {boolean}\n */\nexport function matchesAccept(file, resolvedAccept) {\n if (!resolvedAccept) {\n return true;\n }\n const fileName = file.name.toLowerCase();\n const fileType = (file.type ?? '').toLowerCase();\n return resolvedAccept\n .split(ACCEPT_SEPARATOR)\n .map((rule) => rule.trim().toLowerCase())\n .filter(Boolean)\n .some((rule) => {\n if (rule.startsWith('.')) {\n return fileName.endsWith(rule);\n }\n if (rule.endsWith(WILDCARD_SUFFIX)) {\n return fileType.startsWith(rule.slice(0, -1));\n }\n return fileType === rule;\n });\n}\n\n/**\n * Un tamaño escrito por una persona en bytes: '10 MB' → 10000000.\n *\n * @param {number|string|undefined|null} size\n * @returns {number|null} null si no hay límite\n */\nexport function parseFileSize(size) {\n if (size === undefined || size === null || size === '') {\n return null;\n }\n if (typeof size === 'number') {\n return size;\n }\n const match = SIZE_TEXT.exec(size);\n // guard clause: un límite mal escrito es error de quien configura, no algo que se ignore callado\n if (!match) {\n throw new Error(`«${size}» no es un tamaño: se espera un número de bytes o algo como '10 MB'`);\n }\n return Math.round(Number(match[1]) * BYTES_BY_UNIT[(match[2] ?? 'b').toLowerCase()]);\n}\n\n/**\n * Lo que queda al elegir o soltar archivos: cuáles pasan y cuáles no, con su motivo. La usan los cargadores de MUI del\n * paquete (la zona, el compacto, «Adjuntar» y la imagen), así rechazan igual y con los mismos motivos.\n *\n * Un archivo con el MISMO nombre que uno que ya estaba lo reemplaza (se volvió a elegir corregido) y no cuenta doble ni\n * en la cantidad ni en el peso. Se revisa en este orden: tipo, peso del archivo, cantidad y peso de todos juntos.\n *\n * @param {Array<{name: string, type?: string, size?: number}>} pickedFiles lo que se eligió o se soltó\n * @param {object} rules\n * @param {string} [rules.resolvedAccept] lo que regresa resolveAccept\n * @param {number|null} [rules.maxBytes] el peso máximo de cada archivo\n * @param {number} [rules.maxFiles] cuántos como máximo entre los que ya estaban y los nuevos\n * @param {number|null} [rules.maxTotalBytes] cuánto pueden pesar todos juntos\n * @param {Array<{name: string, size?: number}>} [rules.keptFiles] los que ya estaban (un archivo guardado sin peso cuenta 0)\n * @returns {{files: Array<object>, accepted: Array<object>, rejected: Array<{file: object, reason: string}>}} files: los\n * que ya estaban (sin los reemplazados) y después los nuevos que pasaron\n */\nexport function screenFiles(pickedFiles, {resolvedAccept, maxBytes = null, maxFiles, maxTotalBytes = null, keptFiles = []} = {}) {\n const kept = keptFiles.filter((keptFile) => !pickedFiles.some((picked) => picked.name === keptFile.name));\n const accepted = [];\n const rejected = [];\n let totalBytes = kept.reduce((sum, keptFile) => sum + (keptFile.size ?? 0), 0);\n\n for (const file of pickedFiles) {\n let reason = null;\n if (!matchesAccept(file, resolvedAccept)) {\n reason = FILE_REJECTION.type;\n } else if (maxBytes && file.size > maxBytes) {\n reason = FILE_REJECTION.size;\n } else if (maxFiles && kept.length + accepted.length >= maxFiles) {\n reason = FILE_REJECTION.count;\n } else if (maxTotalBytes && totalBytes + (file.size ?? 0) > maxTotalBytes) {\n reason = FILE_REJECTION.total;\n }\n\n if (reason) {\n rejected.push({file, reason});\n } else {\n accepted.push(file);\n totalBytes += file.size ?? 0;\n }\n }\n\n return {files: [...kept, ...accepted], accepted, rejected};\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const PERCENT = 100;
|
|
2
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
3
|
+
const MIN_SECONDS_FOR_SPEED = 0.5;
|
|
4
|
+
function transferProgressOf({ loaded, total, rate, estimated }, startedAt, now = Date.now()) {
|
|
5
|
+
const elapsedSeconds = (now - startedAt) / MILLISECONDS_PER_SECOND;
|
|
6
|
+
const measuredSpeed = elapsedSeconds >= MIN_SECONDS_FOR_SPEED && loaded > 0 ? loaded / elapsedSeconds : null;
|
|
7
|
+
const bytesPerSecond = rate ?? measuredSpeed;
|
|
8
|
+
let remainingSeconds = estimated ?? null;
|
|
9
|
+
if (remainingSeconds == null && bytesPerSecond) {
|
|
10
|
+
remainingSeconds = Math.max(0, Math.ceil((total - loaded) / bytesPerSecond));
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
percent: total > 0 ? Math.min(PERCENT, Math.round(loaded * PERCENT / total)) : 0,
|
|
14
|
+
loaded,
|
|
15
|
+
total,
|
|
16
|
+
bytesPerSecond,
|
|
17
|
+
remainingSeconds
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
transferProgressOf
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=transferProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transferProgress.js","sources":["../../src/upload/transferProgress.js"],"sourcesContent":["// Cuánto va de una subida, para decirlo en palabras: el porcentaje, lo que va de lo que pesa, la velocidad y cuánto falta.\n// La velocidad y lo que falta salen de lo que ha viajado desde que empezó; si la subida ya los da (axios trae rate y\n// estimated), se usan esos.\n//\n// Puro (sin React): se prueba con node (transferProgress.test.js).\n//\n// Genérico del paquete: function (regla de la casa).\n\nconst PERCENT = 100;\nconst MILLISECONDS_PER_SECOND = 1000;\n// con menos de medio segundo de subida la velocidad todavía brinca: mejor no decirla\nconst MIN_SECONDS_FOR_SPEED = 0.5;\n\n/**\n * @param {{loaded: number, total: number, rate?: number, estimated?: number}} transfer lo que avisa la subida (bytes)\n * @param {number} startedAt cuándo empezó la subida, en milisegundos\n * @param {number} [now]\n * @returns {{percent: number, loaded: number, total: number, bytesPerSecond: number|null, remainingSeconds: number|null}}\n */\nexport function transferProgressOf({loaded, total, rate, estimated}, startedAt, now = Date.now()) {\n const elapsedSeconds = (now - startedAt) / MILLISECONDS_PER_SECOND;\n const measuredSpeed = elapsedSeconds >= MIN_SECONDS_FOR_SPEED && loaded > 0 ? loaded / elapsedSeconds : null;\n const bytesPerSecond = rate ?? measuredSpeed;\n let remainingSeconds = estimated ?? null;\n if (remainingSeconds == null && bytesPerSecond) {\n remainingSeconds = Math.max(0, Math.ceil((total - loaded) / bytesPerSecond));\n }\n return {\n percent: total > 0 ? Math.min(PERCENT, Math.round((loaded * PERCENT) / total)) : 0,\n loaded,\n total,\n bytesPerSecond,\n remainingSeconds,\n };\n}\n"],"names":[],"mappings":"AAQA,MAAM,UAAU;AAChB,MAAM,0BAA0B;AAEhC,MAAM,wBAAwB;AAQvB,SAAS,mBAAmB,EAAC,QAAQ,OAAO,MAAM,UAAS,GAAG,WAAW,MAAM,KAAK,OAAO;AAC9F,QAAM,kBAAkB,MAAM,aAAa;AAC3C,QAAM,gBAAgB,kBAAkB,yBAAyB,SAAS,IAAI,SAAS,iBAAiB;AACxG,QAAM,iBAAiB,QAAQ;AAC/B,MAAI,mBAAmB,aAAa;AACpC,MAAI,oBAAoB,QAAQ,gBAAgB;AAC5C,uBAAmB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,UAAU,cAAc,CAAC;AAAA,EAC/E;AACA,SAAO;AAAA,IACH,SAAS,QAAQ,IAAI,KAAK,IAAI,SAAS,KAAK,MAAO,SAAS,UAAW,KAAK,CAAC,IAAI;AAAA,IACjF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACR;AACA;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assign, setup, fromCallback } from "xstate";
|
|
2
|
-
const resetContext = assign({ progress: null, result: null, problem: null });
|
|
2
|
+
const resetContext = assign({ progress: null, transfer: null, result: null, problem: null });
|
|
3
3
|
const uploadMachine = setup({
|
|
4
4
|
actors: {
|
|
5
5
|
upload: fromCallback(() => {
|
|
@@ -13,7 +13,7 @@ const uploadMachine = setup({
|
|
|
13
13
|
}).createMachine({
|
|
14
14
|
id: "upload",
|
|
15
15
|
initial: "idle",
|
|
16
|
-
context: { progress: null, result: null, problem: null },
|
|
16
|
+
context: { progress: null, transfer: null, result: null, problem: null },
|
|
17
17
|
states: {
|
|
18
18
|
idle: {
|
|
19
19
|
on: { UPLOAD: { target: "uploading", actions: resetContext } }
|
|
@@ -25,8 +25,8 @@ const uploadMachine = setup({
|
|
|
25
25
|
sending: {
|
|
26
26
|
on: {
|
|
27
27
|
PROGRESS: [
|
|
28
|
-
{ guard: "isFullySent", target: "processing", actions: assign({ progress: ({ event }) => event.percent }) },
|
|
29
|
-
{ actions: assign({ progress: ({ event }) => event.percent }) }
|
|
28
|
+
{ guard: "isFullySent", target: "processing", actions: assign({ progress: ({ event }) => event.percent, transfer: ({ event }) => event.transfer ?? null }) },
|
|
29
|
+
{ actions: assign({ progress: ({ event }) => event.percent, transfer: ({ event }) => event.transfer ?? null }) }
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -34,10 +34,10 @@ const uploadMachine = setup({
|
|
|
34
34
|
processing: {}
|
|
35
35
|
},
|
|
36
36
|
on: {
|
|
37
|
-
DONE: { target: "done", actions: assign({ result: ({ event }) => event.output, progress: null }) },
|
|
38
|
-
FAIL: { target: "failed", actions: assign({ problem: ({ event }) => event.problem, progress: null }) },
|
|
37
|
+
DONE: { target: "done", actions: assign({ result: ({ event }) => event.output, progress: null, transfer: null }) },
|
|
38
|
+
FAIL: { target: "failed", actions: assign({ problem: ({ event }) => event.problem, progress: null, transfer: null }) },
|
|
39
39
|
// salir de uploading DETIENE el actor y su cleanup aborta la petición de verdad (no solo la UI)
|
|
40
|
-
CANCEL: { target: "cancelled", actions: assign({ progress: null }) }
|
|
40
|
+
CANCEL: { target: "cancelled", actions: assign({ progress: null, transfer: null }) }
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
done: {
|
|
@@ -56,7 +56,7 @@ function buildUploadActors(uploadFile) {
|
|
|
56
56
|
return {
|
|
57
57
|
upload: fromCallback(({ input, sendBack }) => {
|
|
58
58
|
const abortController = new AbortController();
|
|
59
|
-
uploadFile(input, (percent) => sendBack({ type: "PROGRESS", percent }), abortController.signal).then((output) => sendBack({ type: "DONE", output })).catch((problem) => {
|
|
59
|
+
uploadFile(input, (percent, transfer) => sendBack({ type: "PROGRESS", percent, transfer }), abortController.signal).then((output) => sendBack({ type: "DONE", output })).catch((problem) => {
|
|
60
60
|
if (problem?.name === "AbortError" || problem?.name === "CanceledError") {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadMachine.js","sources":["../../src/upload/uploadMachine.js"],"sourcesContent":["// Máquina del flujo de SUBIDA de un archivo (compañera de una zona para soltar archivos).\n//\n// idle ──UPLOAD──▶ uploading.sending ──(PROGRESS 100%)──▶ uploading.processing\n// │ │\n// ├────────────── DONE ────────────────────┼──▶ done\n// ├────────────── FAIL ────────────────────┴──▶ failed\n// └─── CANCEL (el cleanup aborta la petición) ──▶ cancelled\n//\n// Por qué máquina y no cuatro useState: los estados imposibles dejan de existir (no hay \"resultado y error a la vez\"\n// ni \"progreso sin carga\") y el flujo sending → processing → done queda DECLARADO, no regado en callbacks.\n//\n// Contexto: progress (% de bytes enviados; null fuera de sending) · result (lo que resuelva la subida) · problem (el
|
|
1
|
+
{"version":3,"file":"uploadMachine.js","sources":["../../src/upload/uploadMachine.js"],"sourcesContent":["// Máquina del flujo de SUBIDA de un archivo (compañera de una zona para soltar archivos).\n//\n// idle ──UPLOAD──▶ uploading.sending ──(PROGRESS 100%)──▶ uploading.processing\n// │ │\n// ├────────────── DONE ────────────────────┼──▶ done\n// ├────────────── FAIL ────────────────────┴──▶ failed\n// └─── CANCEL (el cleanup aborta la petición) ──▶ cancelled\n//\n// Por qué máquina y no cuatro useState: los estados imposibles dejan de existir (no hay \"resultado y error a la vez\"\n// ni \"progreso sin carga\") y el flujo sending → processing → done queda DECLARADO, no regado en callbacks.\n//\n// Contexto: progress (% de bytes enviados; null fuera de sending) · transfer (cuánto va y cuánto pesa, {loaded, total}, si\n// la subida lo da) · result (lo que resuelva la subida) · problem (el error RFC 9457 del rechazo, normalmente un ApiError).\n//\n// Uso:\n// const [uploadSnapshot, sendToUpload] = useMachine(uploadMachine.provide({actors: buildUploadActors(uploadFile)}));\n// sendToUpload({type: 'UPLOAD', input: {file}});\n//\n// Pura: solo importa xstate.\nimport {assign, fromCallback, setup} from 'xstate';\n\nconst resetContext = assign({progress: null, transfer: null, result: null, problem: null});\n\nexport const uploadMachine = setup({\n actors: {\n upload: fromCallback(() => {\n throw new Error('uploadMachine: falta proveer upload con buildUploadActors().');\n }),\n },\n guards: {\n // 100% enviado no es terminado: el servidor sigue trabajando\n isFullySent: ({event}) => event.percent >= 100,\n },\n}).createMachine({\n id: 'upload',\n initial: 'idle',\n context: {progress: null, transfer: null, result: null, problem: null},\n states: {\n idle: {\n on: {UPLOAD: {target: 'uploading', actions: resetContext}},\n },\n uploading: {\n invoke: {src: 'upload', input: ({event}) => event.input},\n initial: 'sending',\n states: {\n sending: {\n on: {\n PROGRESS: [\n {guard: 'isFullySent', target: 'processing', actions: assign({progress: ({event}) => event.percent, transfer: ({event}) => event.transfer ?? null})},\n {actions: assign({progress: ({event}) => event.percent, transfer: ({event}) => event.transfer ?? null})},\n ],\n },\n },\n // cuerpo entregado; esperando la respuesta del servidor (progreso indeterminado)\n processing: {},\n },\n on: {\n DONE: {target: 'done', actions: assign({result: ({event}) => event.output, progress: null, transfer: null})},\n FAIL: {target: 'failed', actions: assign({problem: ({event}) => event.problem, progress: null, transfer: null})},\n // salir de uploading DETIENE el actor y su cleanup aborta la petición de verdad (no solo la UI)\n CANCEL: {target: 'cancelled', actions: assign({progress: null, transfer: null})},\n },\n },\n done: {\n on: {UPLOAD: {target: 'uploading', actions: resetContext}, RESET: {target: 'idle', actions: resetContext}},\n },\n failed: {\n on: {UPLOAD: {target: 'uploading', actions: resetContext}, RESET: {target: 'idle', actions: resetContext}},\n },\n // cancelado por la persona: el archivo sigue elegido y se puede reintentar o limpiar\n cancelled: {\n on: {UPLOAD: {target: 'uploading', actions: resetContext}, RESET: {target: 'idle', actions: resetContext}},\n },\n },\n});\n\n/**\n * El actor upload a partir de la función de la pantalla.\n *\n * fromCallback y no fromPromise: hacen falta EVENTOS a medio camino (el avance) y un CLEANUP que aborte la petición.\n *\n * @param {(input: object, onProgress: (percent: number, transfer?: {loaded: number, total: number}) => void, signal: AbortSignal) => Promise<unknown>} uploadFile\n * onProgress lleva, si se sabe, cuánto va y cuánto pesa (para la velocidad y lo que falta)\n */\nexport function buildUploadActors(uploadFile) {\n return {\n upload: fromCallback(({input, sendBack}) => {\n const abortController = new AbortController();\n uploadFile(input, (percent, transfer) => sendBack({type: 'PROGRESS', percent, transfer}), abortController.signal)\n .then((output) => sendBack({type: 'DONE', output}))\n .catch((problem) => {\n // aborto esperado (CANCEL): la máquina ya cambió de estado sola\n if (problem?.name === 'AbortError' || problem?.name === 'CanceledError') {\n return;\n }\n sendBack({type: 'FAIL', problem});\n });\n return () => abortController.abort();\n }),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ function uploadWithProgress(url, formData, { headers = {}, onProgress, signal, w
|
|
|
27
27
|
}
|
|
28
28
|
request.upload.onprogress = (event) => {
|
|
29
29
|
if (event.lengthComputable) {
|
|
30
|
-
onProgress?.(Math.round(event.loaded / event.total * PERCENT));
|
|
30
|
+
onProgress?.(Math.round(event.loaded / event.total * PERCENT), { loaded: event.loaded, total: event.total });
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
request.onload = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploadWithProgress.js","sources":["../../src/upload/uploadWithProgress.js"],"sourcesContent":["// Subir un formulario con archivos (multipart) con el AVANCE real de la subida.\n//\n// fetch no dice cuántos bytes del cuerpo ya viajaron; XMLHttpRequest sí (xhr.upload.onprogress). Es UNA petición normal,\n// el mismo POST de siempre, pero la pantalla puede pintar «Subiendo 47 %».\n//\n// El avance llega a 100 % cuando el CUERPO terminó de viajar; lo que sigue (el servidor procesando y la respuesta\n// bajando) ya no se puede medir aquí: la pantalla cambia a «procesando…» sin porcentaje (uploadMachine lo hace sola).\n//\n// Responde como fetch: un Response (res.ok, res.status, res.headers, res.json(), res.blob()), así quien llama trata el\n// error RFC 9457 igual que con cualquier otra petición. La cancelación también es la de fetch: AbortSignal → AbortError.\n//\n// Funciones del navegador, sin React: function (regla de la casa). Se prueba con node --test (uploadWithProgress.test.js)\n// con un XMLHttpRequest de mentira.\nimport {toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst PERCENT = 100;\n\n/**\n * Los headers de XMLHttpRequest ('k: v\\r\\nk2: v2') como objeto para Response.\n *\n * @param {string|null} rawHeaders\n * @returns {Record<string, string>}\n */\nfunction headersOf(rawHeaders) {\n const headers = {};\n for (const line of String(rawHeaders ?? '').trim().split(/[\\r\\n]+/)) {\n const separatorPosition = line.indexOf(': ');\n if (separatorPosition > 0) {\n headers[line.slice(0, separatorPosition)] = line.slice(separatorPosition + 2);\n }\n }\n return headers;\n}\n\n/**\n * @param {string} url\n * @param {FormData} formData los archivos y los campos que pida el backend\n * @param {object} [options]\n * @param {Record<string, string>} [options.headers] p. ej. Authorization; Accept-Language ya va con el idioma activo\n * @param {(percent: number) => void} [options.onProgress] 0 a 100 de lo que ya viajó del cuerpo\n * @param {AbortSignal} [options.signal]\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio (sesión de Sanctum en otro subdominio)\n * @returns {Promise<Response>}\n */\nexport function uploadWithProgress(url, formData, {headers = {}, onProgress, signal, withCredentials = false} = {}) {\n return new Promise((resolve, reject) => {\n // guard clause: ya venía cancelada\n if (signal?.aborted) {\n reject(new DOMException('La subida se canceló', 'AbortError'));\n return;\n }\n\n const request = new XMLHttpRequest();\n request.open('POST', url);\n request.responseType = 'blob';\n request.withCredentials = withCredentials;\n signal?.addEventListener('abort', () => request.abort(), {once: true});\n\n // OJO: Content-Type NO se pone a mano: el navegador lo arma con el separador del multipart; puesto a mano, el\n // servidor no encuentra los archivos\n const finalHeaders = {'Accept-Language': toolkitSettings().language, ...headers};\n for (const [name, value] of Object.entries(finalHeaders)) {\n request.setRequestHeader(name, value);\n }\n\n request.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * PERCENT));\n }\n };\n request.onload = () => {\n resolve(new Response(request.response, {status: request.status, headers: headersOf(request.getAllResponseHeaders())}));\n };\n request.onerror = () => reject(new TypeError('No hubo respuesta del servidor'));\n request.ontimeout = () => reject(new TypeError('El servidor tardó demasiado en responder'));\n request.onabort = () => reject(new DOMException('La subida se canceló', 'AbortError'));\n request.send(formData);\n });\n}\n"],"names":[],"mappings":";AAeA,MAAM,UAAU;AAQhB,SAAS,UAAU,YAAY;AAC3B,QAAM,UAAU,CAAA;AAChB,aAAW,QAAQ,OAAO,cAAc,EAAE,EAAE,KAAI,EAAG,MAAM,SAAS,GAAG;AACjE,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAC3C,QAAI,oBAAoB,GAAG;AACvB,cAAQ,KAAK,MAAM,GAAG,iBAAiB,CAAC,IAAI,KAAK,MAAM,oBAAoB,CAAC;AAAA,IAChF;AAAA,EACJ;AACA,SAAO;AACX;
|
|
1
|
+
{"version":3,"file":"uploadWithProgress.js","sources":["../../src/upload/uploadWithProgress.js"],"sourcesContent":["// Subir un formulario con archivos (multipart) con el AVANCE real de la subida.\n//\n// fetch no dice cuántos bytes del cuerpo ya viajaron; XMLHttpRequest sí (xhr.upload.onprogress). Es UNA petición normal,\n// el mismo POST de siempre, pero la pantalla puede pintar «Subiendo 47 %».\n//\n// El avance llega a 100 % cuando el CUERPO terminó de viajar; lo que sigue (el servidor procesando y la respuesta\n// bajando) ya no se puede medir aquí: la pantalla cambia a «procesando…» sin porcentaje (uploadMachine lo hace sola).\n//\n// Responde como fetch: un Response (res.ok, res.status, res.headers, res.json(), res.blob()), así quien llama trata el\n// error RFC 9457 igual que con cualquier otra petición. La cancelación también es la de fetch: AbortSignal → AbortError.\n//\n// Funciones del navegador, sin React: function (regla de la casa). Se prueba con node --test (uploadWithProgress.test.js)\n// con un XMLHttpRequest de mentira.\nimport {toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst PERCENT = 100;\n\n/**\n * Los headers de XMLHttpRequest ('k: v\\r\\nk2: v2') como objeto para Response.\n *\n * @param {string|null} rawHeaders\n * @returns {Record<string, string>}\n */\nfunction headersOf(rawHeaders) {\n const headers = {};\n for (const line of String(rawHeaders ?? '').trim().split(/[\\r\\n]+/)) {\n const separatorPosition = line.indexOf(': ');\n if (separatorPosition > 0) {\n headers[line.slice(0, separatorPosition)] = line.slice(separatorPosition + 2);\n }\n }\n return headers;\n}\n\n/**\n * @param {string} url\n * @param {FormData} formData los archivos y los campos que pida el backend\n * @param {object} [options]\n * @param {Record<string, string>} [options.headers] p. ej. Authorization; Accept-Language ya va con el idioma activo\n * @param {(percent: number, transfer: {loaded: number, total: number}) => void} [options.onProgress] 0 a 100 de lo que ya\n * viajó del cuerpo, y los bytes (para la velocidad y lo que falta)\n * @param {AbortSignal} [options.signal]\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio (sesión de Sanctum en otro subdominio)\n * @returns {Promise<Response>}\n */\nexport function uploadWithProgress(url, formData, {headers = {}, onProgress, signal, withCredentials = false} = {}) {\n return new Promise((resolve, reject) => {\n // guard clause: ya venía cancelada\n if (signal?.aborted) {\n reject(new DOMException('La subida se canceló', 'AbortError'));\n return;\n }\n\n const request = new XMLHttpRequest();\n request.open('POST', url);\n request.responseType = 'blob';\n request.withCredentials = withCredentials;\n signal?.addEventListener('abort', () => request.abort(), {once: true});\n\n // OJO: Content-Type NO se pone a mano: el navegador lo arma con el separador del multipart; puesto a mano, el\n // servidor no encuentra los archivos\n const finalHeaders = {'Accept-Language': toolkitSettings().language, ...headers};\n for (const [name, value] of Object.entries(finalHeaders)) {\n request.setRequestHeader(name, value);\n }\n\n request.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * PERCENT), {loaded: event.loaded, total: event.total});\n }\n };\n request.onload = () => {\n resolve(new Response(request.response, {status: request.status, headers: headersOf(request.getAllResponseHeaders())}));\n };\n request.onerror = () => reject(new TypeError('No hubo respuesta del servidor'));\n request.ontimeout = () => reject(new TypeError('El servidor tardó demasiado en responder'));\n request.onabort = () => reject(new DOMException('La subida se canceló', 'AbortError'));\n request.send(formData);\n });\n}\n"],"names":[],"mappings":";AAeA,MAAM,UAAU;AAQhB,SAAS,UAAU,YAAY;AAC3B,QAAM,UAAU,CAAA;AAChB,aAAW,QAAQ,OAAO,cAAc,EAAE,EAAE,KAAI,EAAG,MAAM,SAAS,GAAG;AACjE,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAC3C,QAAI,oBAAoB,GAAG;AACvB,cAAQ,KAAK,MAAM,GAAG,iBAAiB,CAAC,IAAI,KAAK,MAAM,oBAAoB,CAAC;AAAA,IAChF;AAAA,EACJ;AACA,SAAO;AACX;AAaO,SAAS,mBAAmB,KAAK,UAAU,EAAC,UAAU,CAAA,GAAI,YAAY,QAAQ,kBAAkB,MAAK,IAAI,CAAA,GAAI;AAChH,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEpC,QAAI,QAAQ,SAAS;AACjB,aAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AAC7D;AAAA,IACJ;AAEA,UAAM,UAAU,IAAI,eAAc;AAClC,YAAQ,KAAK,QAAQ,GAAG;AACxB,YAAQ,eAAe;AACvB,YAAQ,kBAAkB;AAC1B,YAAQ,iBAAiB,SAAS,MAAM,QAAQ,SAAS,EAAC,MAAM,KAAI,CAAC;AAIrE,UAAM,eAAe,EAAC,mBAAmB,gBAAe,EAAG,UAAU,GAAG,QAAO;AAC/E,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACtD,cAAQ,iBAAiB,MAAM,KAAK;AAAA,IACxC;AAEA,YAAQ,OAAO,aAAa,CAAC,UAAU;AACnC,UAAI,MAAM,kBAAkB;AACxB,qBAAa,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,OAAO,GAAG,EAAC,QAAQ,MAAM,QAAQ,OAAO,MAAM,MAAK,CAAC;AAAA,MAC/G;AAAA,IACJ;AACA,YAAQ,SAAS,MAAM;AACnB,cAAQ,IAAI,SAAS,QAAQ,UAAU,EAAC,QAAQ,QAAQ,QAAQ,SAAS,UAAU,QAAQ,sBAAqB,CAAE,EAAC,CAAC,CAAC;AAAA,IACzH;AACA,YAAQ,UAAU,MAAM,OAAO,IAAI,UAAU,gCAAgC,CAAC;AAC9E,YAAQ,YAAY,MAAM,OAAO,IAAI,UAAU,0CAA0C,CAAC;AAC1F,YAAQ,UAAU,MAAM,OAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AACrF,YAAQ,KAAK,QAAQ;AAAA,EACzB,CAAC;AACL;"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { toastError } from "../feedback/toast.js";
|
|
4
|
+
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
5
|
+
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
6
|
+
import { resolveAccept, parseFileSize, screenFiles, FILE_REJECTION } from "./fileRules.js";
|
|
7
|
+
function useFilePicker({
|
|
8
|
+
accept,
|
|
9
|
+
maxSize,
|
|
10
|
+
multiple = false,
|
|
11
|
+
maxFiles,
|
|
12
|
+
maxTotalSize,
|
|
13
|
+
keptFiles = [],
|
|
14
|
+
disabled = false,
|
|
15
|
+
onAccept,
|
|
16
|
+
onReject,
|
|
17
|
+
texts
|
|
18
|
+
}) {
|
|
19
|
+
const { t } = useToolkitTranslation("common", texts);
|
|
20
|
+
const { formatFileSize } = useFormatters();
|
|
21
|
+
const inputRef = useRef(null);
|
|
22
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
23
|
+
const resolvedAccept = resolveAccept(accept);
|
|
24
|
+
const maxBytes = parseFileSize(maxSize);
|
|
25
|
+
const maxTotalBytes = multiple ? parseFileSize(maxTotalSize) : null;
|
|
26
|
+
const rejectionTextOf = (file, reason) => {
|
|
27
|
+
switch (reason) {
|
|
28
|
+
case FILE_REJECTION.size:
|
|
29
|
+
return t("upload.tooLarge", { name: file.name, max: formatFileSize(maxBytes) });
|
|
30
|
+
case FILE_REJECTION.count:
|
|
31
|
+
return t("upload.tooManyFiles", { count: maxFiles });
|
|
32
|
+
case FILE_REJECTION.total:
|
|
33
|
+
return t("upload.totalTooLarge", { name: file.name, max: formatFileSize(maxTotalBytes) });
|
|
34
|
+
default:
|
|
35
|
+
return t("upload.invalidType", { name: file.name, accept: resolvedAccept });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const pickFiles = (fileList) => {
|
|
39
|
+
const picked = [...fileList ?? []].slice(0, multiple ? void 0 : 1);
|
|
40
|
+
if (!picked.length || disabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const { files, accepted, rejected } = screenFiles(picked, {
|
|
44
|
+
resolvedAccept,
|
|
45
|
+
maxBytes,
|
|
46
|
+
maxFiles: multiple ? maxFiles : void 0,
|
|
47
|
+
maxTotalBytes,
|
|
48
|
+
keptFiles: multiple ? keptFiles : []
|
|
49
|
+
});
|
|
50
|
+
for (const { file, reason } of rejected) {
|
|
51
|
+
if (onReject) {
|
|
52
|
+
onReject(file, reason);
|
|
53
|
+
} else {
|
|
54
|
+
toastError(rejectionTextOf(file, reason));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (accepted.length) {
|
|
58
|
+
onAccept(multiple ? files : accepted[0]);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const open = () => {
|
|
62
|
+
if (disabled || !inputRef.current) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
inputRef.current.value = "";
|
|
66
|
+
inputRef.current.click();
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
open,
|
|
70
|
+
pickFiles,
|
|
71
|
+
isDragging,
|
|
72
|
+
resolvedAccept,
|
|
73
|
+
maxBytes,
|
|
74
|
+
maxTotalBytes,
|
|
75
|
+
inputProps: {
|
|
76
|
+
ref: inputRef,
|
|
77
|
+
type: "file",
|
|
78
|
+
accept: resolvedAccept,
|
|
79
|
+
multiple,
|
|
80
|
+
hidden: true,
|
|
81
|
+
disabled,
|
|
82
|
+
onChange: (event) => pickFiles(event.target.files)
|
|
83
|
+
},
|
|
84
|
+
dropHandlers: {
|
|
85
|
+
onDragOver: (event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
setIsDragging(!disabled);
|
|
88
|
+
},
|
|
89
|
+
onDragLeave: () => setIsDragging(false),
|
|
90
|
+
onDrop: (event) => {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
setIsDragging(false);
|
|
93
|
+
pickFiles(event.dataTransfer?.files);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
useFilePicker
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=useFilePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFilePicker.js","sources":["../../src/upload/useFilePicker.js"],"sourcesContent":["// Lo que necesita cualquier cargador para ELEGIR archivos: la ventana de archivos del sistema, soltar encima, revisar tipo,\n// peso, cuántos y cuánto pesan juntos (screenFiles), y avisar lo que no pasó. Lo usan los cargadores del paquete (la zona,\n// el compacto, «Adjuntar» y la imagen) y queda suelto para que un proyecto arme el suyo con su propio botón:\n//\n// const picker = useFilePicker({accept: 'pdf', maxSize: '5 MB', onAccept: setFile});\n// <input {...picker.inputProps} />\n// <Button onClick={picker.open} {...picker.dropHandlers}>Subir el contrato</Button>\n//\n// Solo elige: enviarlo es de la pantalla (uploadWithProgress). Lo rechazado se avisa con toastError y los textos del\n// paquete; con onReject la pantalla decide qué hacer.\n//\n// Hook genérico: function (regla de la casa).\nimport {useRef, useState} from 'react';\n\nimport {toastError} from '@/feedback/toast.jsx';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {FILE_REJECTION, parseFileSize, resolveAccept, screenFiles} from '@/upload/fileRules.js';\n\n/**\n * @param {object} options\n * @param {string|string[]} [options.accept] 'excel' | ['excel', 'pdf'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [options.maxSize] el peso máximo de cada archivo: bytes o '10 MB'\n * @param {boolean} [options.multiple] varios a la vez; sin él, solo el primero que se elija o se suelte\n * @param {number} [options.maxFiles] con multiple: cuántos como máximo, contando los que ya estaban\n * @param {number|string} [options.maxTotalSize] con multiple: cuánto pueden pesar todos juntos\n * @param {Array<{name: string, size?: number}>} [options.keptFiles] con multiple: los que ya estaban (cuentan en los topes)\n * @param {boolean} [options.disabled] no se puede elegir ni soltar (también mientras se sube)\n * @param {(picked: any) => void} options.onAccept sin multiple, el archivo; con multiple, la lista completa (los que ya\n * estaban y los nuevos), lista para guardarse\n * @param {(file: File, reason: string) => void} [options.onReject] reason es uno de FILE_REJECTION; sin él, un aviso\n * @param {object} [options.texts] los textos que cambian, con la forma de `common`\n * @returns {{inputProps: object, open: () => void, pickFiles: (fileList: FileList|File[]|null|undefined) => void, isDragging: boolean, dropHandlers: object, resolvedAccept: string|undefined, maxBytes: number|null, maxTotalBytes: number|null}}\n */\nexport function useFilePicker({\n accept, maxSize, multiple = false, maxFiles, maxTotalSize, keptFiles = [], disabled = false, onAccept, onReject, texts,\n}) {\n const {t} = useToolkitTranslation('common', texts);\n const {formatFileSize} = useFormatters();\n const inputRef = useRef(null);\n const [isDragging, setIsDragging] = useState(false);\n const resolvedAccept = resolveAccept(accept);\n const maxBytes = parseFileSize(maxSize);\n const maxTotalBytes = multiple ? parseFileSize(maxTotalSize) : null;\n\n // el aviso de cada motivo, con el nombre del archivo y el límite que no cumplió\n const rejectionTextOf = (file, reason) => {\n switch (reason) {\n case FILE_REJECTION.size:\n return t('upload.tooLarge', {name: file.name, max: formatFileSize(maxBytes)});\n case FILE_REJECTION.count:\n return t('upload.tooManyFiles', {count: maxFiles});\n case FILE_REJECTION.total:\n return t('upload.totalTooLarge', {name: file.name, max: formatFileSize(maxTotalBytes)});\n default:\n return t('upload.invalidType', {name: file.name, accept: resolvedAccept});\n }\n };\n\n const pickFiles = (fileList) => {\n // se cerró la ventana sin elegir, o el candado está puesto\n const picked = [...(fileList ?? [])].slice(0, multiple ? undefined : 1);\n if (!picked.length || disabled) {\n return;\n }\n const {files, accepted, rejected} = screenFiles(picked, {\n resolvedAccept, maxBytes, maxFiles: multiple ? maxFiles : undefined, maxTotalBytes, keptFiles: multiple ? keptFiles : [],\n });\n for (const {file, reason} of rejected) {\n if (onReject) {\n onReject(file, reason);\n } else {\n toastError(rejectionTextOf(file, reason));\n }\n }\n if (accepted.length) {\n onAccept(multiple ? files : accepted[0]);\n }\n };\n\n const open = () => {\n if (disabled || !inputRef.current) {\n return;\n }\n // OJO: se limpia el valor del input para poder volver a elegir EL MISMO archivo (corregido en disco); sin esto el\n // navegador no avisa el cambio\n inputRef.current.value = '';\n inputRef.current.click();\n };\n\n return {\n open,\n pickFiles,\n isDragging,\n resolvedAccept,\n maxBytes,\n maxTotalBytes,\n inputProps: {\n ref: inputRef, type: 'file', accept: resolvedAccept, multiple, hidden: true, disabled,\n onChange: (event) => pickFiles(event.target.files),\n },\n dropHandlers: {\n onDragOver: (event) => {\n event.preventDefault();\n setIsDragging(!disabled);\n },\n onDragLeave: () => setIsDragging(false),\n onDrop: (event) => {\n event.preventDefault();\n setIsDragging(false);\n pickFiles(event.dataTransfer?.files);\n },\n },\n };\n}\n"],"names":[],"mappings":";;;;;;AAkCO,SAAS,cAAc;AAAA,EAC1B;AAAA,EAAQ;AAAA,EAAS,WAAW;AAAA,EAAO;AAAA,EAAU;AAAA,EAAc,YAAY,CAAA;AAAA,EAAI,WAAW;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU;AACrH,GAAG;AACC,QAAM,EAAC,EAAC,IAAI,sBAAsB,UAAU,KAAK;AACjD,QAAM,EAAC,eAAc,IAAI,cAAa;AACtC,QAAM,WAAW,OAAO,IAAI;AAC5B,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,iBAAiB,cAAc,MAAM;AAC3C,QAAM,WAAW,cAAc,OAAO;AACtC,QAAM,gBAAgB,WAAW,cAAc,YAAY,IAAI;AAG/D,QAAM,kBAAkB,CAAC,MAAM,WAAW;AACtC,YAAQ,QAAM;AAAA,MACV,KAAK,eAAe;AAChB,eAAO,EAAE,mBAAmB,EAAC,MAAM,KAAK,MAAM,KAAK,eAAe,QAAQ,EAAC,CAAC;AAAA,MAChF,KAAK,eAAe;AAChB,eAAO,EAAE,uBAAuB,EAAC,OAAO,SAAQ,CAAC;AAAA,MACrD,KAAK,eAAe;AAChB,eAAO,EAAE,wBAAwB,EAAC,MAAM,KAAK,MAAM,KAAK,eAAe,aAAa,EAAC,CAAC;AAAA,MAC1F;AACI,eAAO,EAAE,sBAAsB,EAAC,MAAM,KAAK,MAAM,QAAQ,eAAc,CAAC;AAAA,IACxF;AAAA,EACI;AAEA,QAAM,YAAY,CAAC,aAAa;AAE5B,UAAM,SAAS,CAAC,GAAI,YAAY,EAAG,EAAE,MAAM,GAAG,WAAW,SAAY,CAAC;AACtE,QAAI,CAAC,OAAO,UAAU,UAAU;AAC5B;AAAA,IACJ;AACA,UAAM,EAAC,OAAO,UAAU,SAAQ,IAAI,YAAY,QAAQ;AAAA,MACpD;AAAA,MAAgB;AAAA,MAAU,UAAU,WAAW,WAAW;AAAA,MAAW;AAAA,MAAe,WAAW,WAAW,YAAY,CAAA;AAAA,IAClI,CAAS;AACD,eAAW,EAAC,MAAM,OAAM,KAAK,UAAU;AACnC,UAAI,UAAU;AACV,iBAAS,MAAM,MAAM;AAAA,MACzB,OAAO;AACH,mBAAW,gBAAgB,MAAM,MAAM,CAAC;AAAA,MAC5C;AAAA,IACJ;AACA,QAAI,SAAS,QAAQ;AACjB,eAAS,WAAW,QAAQ,SAAS,CAAC,CAAC;AAAA,IAC3C;AAAA,EACJ;AAEA,QAAM,OAAO,MAAM;AACf,QAAI,YAAY,CAAC,SAAS,SAAS;AAC/B;AAAA,IACJ;AAGA,aAAS,QAAQ,QAAQ;AACzB,aAAS,QAAQ,MAAK;AAAA,EAC1B;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,MACR,KAAK;AAAA,MAAU,MAAM;AAAA,MAAQ,QAAQ;AAAA,MAAgB;AAAA,MAAU,QAAQ;AAAA,MAAM;AAAA,MAC7E,UAAU,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK;AAAA,IAC7D;AAAA,IACQ,cAAc;AAAA,MACV,YAAY,CAAC,UAAU;AACnB,cAAM,eAAc;AACpB,sBAAc,CAAC,QAAQ;AAAA,MAC3B;AAAA,MACA,aAAa,MAAM,cAAc,KAAK;AAAA,MACtC,QAAQ,CAAC,UAAU;AACf,cAAM,eAAc;AACpB,sBAAc,KAAK;AACnB,kBAAU,MAAM,cAAc,KAAK;AAAA,MACvC;AAAA,IACZ;AAAA,EACA;AACA;"}
|