@stackcraftmx/calcita 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +109 -4
  2. package/dist/datePickers/DateInput.js +5 -3
  3. package/dist/datePickers/DateInput.js.map +1 -1
  4. package/dist/datePickers/DateTimeInput.js +6 -4
  5. package/dist/datePickers/DateTimeInput.js.map +1 -1
  6. package/dist/datePickers/FormDateField.js +1 -1
  7. package/dist/datePickers/FormDateField.js.map +1 -1
  8. package/dist/datePickers/FormDateTimeField.js +1 -1
  9. package/dist/datePickers/FormDateTimeField.js.map +1 -1
  10. package/dist/datePickers/FormMonthField.js +1 -1
  11. package/dist/datePickers/FormMonthField.js.map +1 -1
  12. package/dist/datePickers/FormTimeField.js +1 -1
  13. package/dist/datePickers/FormTimeField.js.map +1 -1
  14. package/dist/datePickers/MonthInput.js +5 -3
  15. package/dist/datePickers/MonthInput.js.map +1 -1
  16. package/dist/datePickers/PickerFrame.js +2 -2
  17. package/dist/datePickers/PickerFrame.js.map +1 -1
  18. package/dist/datePickers/TimeInput.js +6 -4
  19. package/dist/datePickers/TimeInput.js.map +1 -1
  20. package/dist/feedback/Toaster.js +11 -2
  21. package/dist/feedback/Toaster.js.map +1 -1
  22. package/dist/filepond/FileDropzone.js +60 -19
  23. package/dist/filepond/FileDropzone.js.map +1 -1
  24. package/dist/filepond/pondServer.js +86 -27
  25. package/dist/filepond/pondServer.js.map +1 -1
  26. package/dist/forms/CalcitaFormik.js +11 -0
  27. package/dist/forms/CalcitaFormik.js.map +1 -0
  28. package/dist/forms/FormAttachmentsField.js +1 -1
  29. package/dist/forms/FormAttachmentsField.js.map +1 -1
  30. package/dist/forms/FormCatalogField.js +1 -1
  31. package/dist/forms/FormCatalogField.js.map +1 -1
  32. package/dist/forms/FormCheckboxGroupField.js +1 -1
  33. package/dist/forms/FormCheckboxGroupField.js.map +1 -1
  34. package/dist/forms/FormColorField.js +1 -1
  35. package/dist/forms/FormColorField.js.map +1 -1
  36. package/dist/forms/FormFileField.js +1 -1
  37. package/dist/forms/FormFileField.js.map +1 -1
  38. package/dist/forms/FormFileUploadField.js +1 -1
  39. package/dist/forms/FormFileUploadField.js.map +1 -1
  40. package/dist/forms/FormImageField.js +1 -1
  41. package/dist/forms/FormImageField.js.map +1 -1
  42. package/dist/forms/FormMaskedField.js +1 -1
  43. package/dist/forms/FormMaskedField.js.map +1 -1
  44. package/dist/forms/FormNumberField.js +1 -1
  45. package/dist/forms/FormNumberField.js.map +1 -1
  46. package/dist/forms/FormSchemaProvider.js +17 -0
  47. package/dist/forms/FormSchemaProvider.js.map +1 -0
  48. package/dist/forms/FormSelectField.js +1 -1
  49. package/dist/forms/FormSelectField.js.map +1 -1
  50. package/dist/forms/FormTextField.js +1 -1
  51. package/dist/forms/FormTextField.js.map +1 -1
  52. package/dist/forms/FormTransferListField.js +1 -1
  53. package/dist/forms/FormTransferListField.js.map +1 -1
  54. package/dist/forms/FormWizard.js +2 -0
  55. package/dist/forms/FormWizard.js.map +1 -1
  56. package/dist/forms/schemaRequired.js +33 -0
  57. package/dist/forms/schemaRequired.js.map +1 -0
  58. package/dist/forms/useFormikField.js +18 -5
  59. package/dist/forms/useFormikField.js.map +1 -1
  60. package/dist/forms.js +52 -46
  61. package/dist/forms.js.map +1 -1
  62. package/dist/hookForm/FormAttachmentsField.js +1 -1
  63. package/dist/hookForm/FormAttachmentsField.js.map +1 -1
  64. package/dist/hookForm/FormCatalogField.js +1 -1
  65. package/dist/hookForm/FormCatalogField.js.map +1 -1
  66. package/dist/hookForm/FormCheckboxGroupField.js +1 -1
  67. package/dist/hookForm/FormCheckboxGroupField.js.map +1 -1
  68. package/dist/hookForm/FormColorField.js +1 -1
  69. package/dist/hookForm/FormColorField.js.map +1 -1
  70. package/dist/hookForm/FormFileField.js +1 -1
  71. package/dist/hookForm/FormFileField.js.map +1 -1
  72. package/dist/hookForm/FormFileUploadField.js +1 -1
  73. package/dist/hookForm/FormFileUploadField.js.map +1 -1
  74. package/dist/hookForm/FormImageField.js +1 -1
  75. package/dist/hookForm/FormImageField.js.map +1 -1
  76. package/dist/hookForm/FormMaskedField.js +1 -1
  77. package/dist/hookForm/FormMaskedField.js.map +1 -1
  78. package/dist/hookForm/FormNumberField.js +1 -1
  79. package/dist/hookForm/FormNumberField.js.map +1 -1
  80. package/dist/hookForm/FormSelectField.js +1 -1
  81. package/dist/hookForm/FormSelectField.js.map +1 -1
  82. package/dist/hookForm/FormTextField.js +1 -1
  83. package/dist/hookForm/FormTextField.js.map +1 -1
  84. package/dist/hookForm/FormTransferListField.js +1 -1
  85. package/dist/hookForm/FormTransferListField.js.map +1 -1
  86. package/dist/hookForm/FormWizard.js +2 -0
  87. package/dist/hookForm/FormWizard.js.map +1 -1
  88. package/dist/hookForm/useHookFormField.js +9 -3
  89. package/dist/hookForm/useHookFormField.js.map +1 -1
  90. package/dist/index.js +6 -2
  91. package/dist/index.js.map +1 -1
  92. package/dist/inputs/CatalogInput.js +3 -2
  93. package/dist/inputs/CatalogInput.js.map +1 -1
  94. package/dist/inputs/CheckboxGroupInput.js +2 -1
  95. package/dist/inputs/CheckboxGroupInput.js.map +1 -1
  96. package/dist/inputs/ColorInput.js +3 -1
  97. package/dist/inputs/ColorInput.js.map +1 -1
  98. package/dist/inputs/FieldFrame.js +18 -3
  99. package/dist/inputs/FieldFrame.js.map +1 -1
  100. package/dist/inputs/FormSection.js +42 -11
  101. package/dist/inputs/FormSection.js.map +1 -1
  102. package/dist/inputs/NumberInput.js +3 -1
  103. package/dist/inputs/NumberInput.js.map +1 -1
  104. package/dist/inputs/SelectInput.js +3 -1
  105. package/dist/inputs/SelectInput.js.map +1 -1
  106. package/dist/inputs/TextInput.js +1 -1
  107. package/dist/inputs/TextInput.js.map +1 -1
  108. package/dist/inputs/TransferListInput.js +2 -1
  109. package/dist/inputs/TransferListInput.js.map +1 -1
  110. package/dist/inputs/WizardLayout.js +12 -6
  111. package/dist/inputs/WizardLayout.js.map +1 -1
  112. package/dist/inputs/formLayoutContext.js +7 -2
  113. package/dist/inputs/formLayoutContext.js.map +1 -1
  114. package/dist/inputs/requiredMark.js +17 -0
  115. package/dist/inputs/requiredMark.js.map +1 -0
  116. package/dist/locales/en.js +2 -0
  117. package/dist/locales/en.js.map +1 -1
  118. package/dist/locales/es.js +2 -0
  119. package/dist/locales/es.js.map +1 -1
  120. package/dist/richText/FormRichTextField.js +1 -1
  121. package/dist/richText/FormRichTextField.js.map +1 -1
  122. package/dist/richText/RichTextEditor.js +4 -3
  123. package/dist/richText/RichTextEditor.js.map +1 -1
  124. package/dist/toolkit/ToolkitProvider.js +3 -0
  125. package/dist/toolkit/ToolkitProvider.js.map +1 -1
  126. package/dist/toolkit/toolkitContext.js.map +1 -1
  127. package/dist/types/datePickers/DateInput.d.ts +2 -0
  128. package/dist/types/datePickers/DateTimeInput.d.ts +2 -0
  129. package/dist/types/datePickers/FormDateField.d.ts +3 -3
  130. package/dist/types/datePickers/FormDateTimeField.d.ts +3 -3
  131. package/dist/types/datePickers/FormMonthField.d.ts +3 -3
  132. package/dist/types/datePickers/FormTimeField.d.ts +3 -3
  133. package/dist/types/datePickers/MonthInput.d.ts +2 -0
  134. package/dist/types/datePickers/PickerFrame.d.ts +3 -1
  135. package/dist/types/datePickers/TimeInput.d.ts +2 -0
  136. package/dist/types/feedback/Toaster.d.ts +15 -1
  137. package/dist/types/feedback/index.d.ts +1 -1
  138. package/dist/types/filepond/FileDropzone.d.ts +5 -0
  139. package/dist/types/filepond/chunkedUpload.d.ts +122 -0
  140. package/dist/types/filepond/pondServer.d.ts +25 -3
  141. package/dist/types/forms/CalcitaFormik.d.ts +6 -0
  142. package/dist/types/forms/FormAttachmentsField.d.ts +3 -3
  143. package/dist/types/forms/FormCatalogField.d.ts +2 -2
  144. package/dist/types/forms/FormCheckboxGroupField.d.ts +3 -3
  145. package/dist/types/forms/FormColorField.d.ts +2 -2
  146. package/dist/types/forms/FormFileField.d.ts +3 -3
  147. package/dist/types/forms/FormFileUploadField.d.ts +3 -3
  148. package/dist/types/forms/FormImageField.d.ts +3 -3
  149. package/dist/types/forms/FormMaskedField.d.ts +2 -2
  150. package/dist/types/forms/FormNumberField.d.ts +2 -2
  151. package/dist/types/forms/FormSchemaProvider.d.ts +13 -0
  152. package/dist/types/forms/FormSelectField.d.ts +2 -2
  153. package/dist/types/forms/FormTextField.d.ts +2 -2
  154. package/dist/types/forms/FormTransferListField.d.ts +3 -3
  155. package/dist/types/forms/FormWizard.d.ts +3 -1
  156. package/dist/types/forms/index.d.ts +1 -0
  157. package/dist/types/forms/schemaRequired.d.ts +14 -0
  158. package/dist/types/forms/useFormikField.d.ts +10 -4
  159. package/dist/types/forms.d.ts +4 -2
  160. package/dist/types/hookForm/FormAttachmentsField.d.ts +3 -3
  161. package/dist/types/hookForm/FormCatalogField.d.ts +3 -3
  162. package/dist/types/hookForm/FormCheckboxGroupField.d.ts +3 -3
  163. package/dist/types/hookForm/FormColorField.d.ts +3 -3
  164. package/dist/types/hookForm/FormFileField.d.ts +3 -3
  165. package/dist/types/hookForm/FormFileUploadField.d.ts +3 -3
  166. package/dist/types/hookForm/FormImageField.d.ts +3 -3
  167. package/dist/types/hookForm/FormMaskedField.d.ts +3 -3
  168. package/dist/types/hookForm/FormNumberField.d.ts +3 -3
  169. package/dist/types/hookForm/FormSelectField.d.ts +3 -3
  170. package/dist/types/hookForm/FormTextField.d.ts +3 -3
  171. package/dist/types/hookForm/FormTransferListField.d.ts +3 -3
  172. package/dist/types/hookForm/FormWizard.d.ts +3 -1
  173. package/dist/types/hookForm/useHookFormField.d.ts +8 -4
  174. package/dist/types/index.d.ts +3 -2
  175. package/dist/types/inputs/CatalogInput.d.ts +1 -0
  176. package/dist/types/inputs/CheckboxGroupInput.d.ts +1 -0
  177. package/dist/types/inputs/ColorInput.d.ts +1 -0
  178. package/dist/types/inputs/FieldFrame.d.ts +1 -0
  179. package/dist/types/inputs/FormSection.d.ts +13 -0
  180. package/dist/types/inputs/NumberInput.d.ts +1 -0
  181. package/dist/types/inputs/SelectInput.d.ts +1 -0
  182. package/dist/types/inputs/TextInput.d.ts +1 -1
  183. package/dist/types/inputs/TransferListInput.d.ts +1 -0
  184. package/dist/types/inputs/WizardLayout.d.ts +1 -0
  185. package/dist/types/inputs/formLayoutContext.d.ts +8 -1
  186. package/dist/types/inputs/requiredMark.d.ts +9 -0
  187. package/dist/types/locales/en.d.ts +2 -0
  188. package/dist/types/locales/es.d.ts +2 -0
  189. package/dist/types/richText/FormRichTextField.d.ts +3 -3
  190. package/dist/types/richText/RichTextEditor.d.ts +1 -0
  191. package/dist/types/toolkit/ToolkitProvider.d.ts +5 -1
  192. package/dist/types/toolkit/toolkitContext.d.ts +2 -1
  193. package/dist/types/upload/AttachmentsInput.d.ts +2 -0
  194. package/dist/types/upload/FileInput.d.ts +1 -0
  195. package/dist/types/upload/FileUploadInput.d.ts +1 -0
  196. package/dist/types/upload/ImageInput.d.ts +1 -0
  197. package/dist/types/upload/chunkedUpload.d.ts +189 -0
  198. package/dist/types/upload/uploadWithProgress.d.ts +4 -2
  199. package/dist/upload/AttachmentsInput.js +2 -1
  200. package/dist/upload/AttachmentsInput.js.map +1 -1
  201. package/dist/upload/FileInput.js +2 -1
  202. package/dist/upload/FileInput.js.map +1 -1
  203. package/dist/upload/FileUploadInput.js +2 -1
  204. package/dist/upload/FileUploadInput.js.map +1 -1
  205. package/dist/upload/ImageInput.js +2 -1
  206. package/dist/upload/ImageInput.js.map +1 -1
  207. package/dist/upload/chunkedUpload.js +261 -0
  208. package/dist/upload/chunkedUpload.js.map +1 -0
  209. package/dist/upload/uploadWithProgress.js +8 -3
  210. package/dist/upload/uploadWithProgress.js.map +1 -1
  211. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ /** Los headers del protocolo de trozos de FilePond. */
2
+ export declare const CHUNK_HEADER: Readonly<{
3
+ length: "Upload-Length";
4
+ offset: "Upload-Offset";
5
+ name: "Upload-Name";
6
+ contentType: "Content-Type";
7
+ }>;
8
+ /** Por qué no se pudo subir: el servidor dijo que no, no contestó, o no dio el folio de la subida. */
9
+ export declare const UPLOAD_FAILURE: Readonly<{
10
+ rejected: "rejected";
11
+ noResponse: "noResponse";
12
+ noTransferId: "noTransferId";
13
+ }>;
14
+ /** Los de FilePond: trozos de 5 MB y tres reintentos por trozo. */
15
+ export declare const DEFAULT_CHUNK_SIZE = 5000000;
16
+ export declare const DEFAULT_CHUNK_RETRY_DELAYS: readonly number[];
17
+ /** Una subida que no salió: el motivo (UPLOAD_FAILURE) y la respuesta del servidor, si la hubo. */
18
+ export declare class UploadFailure extends Error {
19
+ reason: string;
20
+ response: Response | null;
21
+ /**
22
+ * @param {string} reason de UPLOAD_FAILURE
23
+ * @param {Response|null} [response]
24
+ */
25
+ constructor(reason: string, response?: Response | null);
26
+ }
27
+ /**
28
+ * Los trozos de un archivo: dónde empieza y dónde acaba cada uno. Un archivo vacío es un trozo vacío: el servidor igual
29
+ * necesita saber que el archivo llegó completo.
30
+ *
31
+ * @param {number} fileSize en bytes
32
+ * @param {number} chunkSize en bytes
33
+ * @returns {{index: number, offset: number, end: number}[]}
34
+ */
35
+ export declare function chunkPiecesOf(fileSize: number, chunkSize: number): {
36
+ index: number;
37
+ offset: number;
38
+ end: number;
39
+ }[];
40
+ /**
41
+ * Lo que la barra puede decir que ya subió: lo que el servidor confirmó más lo que va en camino del trozo de ahora, sin
42
+ * llegar al final hasta que el servidor confirma el último trozo.
43
+ *
44
+ * @param {{confirmedBytes: number, sendingBytes: number, totalBytes: number}} transfer
45
+ * @returns {number} bytes
46
+ */
47
+ export declare function honestLoadedOf({ confirmedBytes, sendingBytes, totalBytes }: {
48
+ confirmedBytes: number;
49
+ sendingBytes: number;
50
+ totalBytes: number;
51
+ }): number;
52
+ /**
53
+ * Hasta dónde ya está guardado el archivo cuando el servidor dice su Upload-Offset: el final del último trozo COMPLETO antes
54
+ * de ese punto. Un trozo que quedó a medias se vuelve a mandar entero.
55
+ *
56
+ * @param {{offset: number, end: number}[]} pieces
57
+ * @param {number} serverOffset
58
+ * @returns {number}
59
+ */
60
+ export declare function storedBytesOf(pieces: {
61
+ offset: number;
62
+ end: number;
63
+ }[], serverOffset: number): number;
64
+ /**
65
+ * Un texto listo para viajar en un header. Los headers solo llevan bytes: el texto va en UTF-8 byte por byte (lo mismo que
66
+ * FilePond hace con los suyos). Sin esto, un nombre con «–» o con un emoji tumba la petición y uno con acentos llega roto.
67
+ *
68
+ * @param {unknown} value
69
+ * @returns {string}
70
+ */
71
+ export declare function headerTextOf(value: unknown): string;
72
+ export type UploadSendRequest = (url: string, body: FormData | Blob | null, options: {
73
+ method: string;
74
+ headers: Record<string, string>;
75
+ withCredentials: boolean;
76
+ signal: AbortSignal;
77
+ onProgress?: (percent: number, transfer: {
78
+ loaded: number;
79
+ total: number;
80
+ }) => void;
81
+ }) => Promise<Response>;
82
+ /**
83
+ * @typedef {(url: string, body: FormData|Blob|null, options: {method: string, headers: Record<string, string>, withCredentials: boolean, signal: AbortSignal, onProgress?: (percent: number, transfer: {loaded: number, total: number}) => void}) => Promise<Response>} UploadSendRequest
84
+ * cómo viaja cada petición: la misma forma que uploadWithProgress de la casa
85
+ */
86
+ /**
87
+ * A dónde van los trozos si no se dice: lo que usa FilePond, «?patch=» pegado a url (o «&patch=» si url ya trae «?»).
88
+ *
89
+ * @param {string} url
90
+ * @returns {string}
91
+ */
92
+ export declare function defaultPatchUrlOf(url: string): string;
93
+ /**
94
+ * El problema del cuerpo de error: el JSON del servidor, pasado por el adaptador del proyecto (toProblem). Lo que no sea un
95
+ * objeto cuenta como sin problema (se usa el texto genérico del paquete). Un adaptador que truena no tumba el mensaje.
96
+ *
97
+ * @param {Response} response
98
+ * @param {((body: any, response: Response) => any)|undefined} toProblem
99
+ * @returns {Promise<object|null>}
100
+ */
101
+ export declare function problemOfResponse(response: Response, toProblem: ((body: any, response: Response) => any) | undefined): Promise<object | null>;
102
+ /**
103
+ * Sube un archivo por trozos (ver el encabezado). Regresa la subida en curso: done termina con el folio que dio el servidor
104
+ * y abort la cancela de verdad (la petición de ahora y los reintentos que esperaban).
105
+ *
106
+ * @param {object} options
107
+ * @param {Blob} options.file
108
+ * @param {string} options.fileName el que viaja en Upload-Name
109
+ * @param {string} options.fieldName el campo del primer POST: lleva los metadatos en JSON, como FilePond
110
+ * @param {object|null} [options.metadata]
111
+ * @param {string} options.url a dónde va el POST que pide el folio
112
+ * @param {string} options.patchUrl a dónde va cada trozo: el folio va pegado al final ('/subidas?patch=')
113
+ * @param {Record<string, unknown>} [options.headers] van en las tres peticiones (Authorization, los datos de la pantalla)
114
+ * @param {boolean} [options.withCredentials]
115
+ * @param {number} options.chunkSize en bytes
116
+ * @param {number[]|null} [options.retryDelays] las esperas entre reintentos, en milisegundos; null o [] = sin reintentos
117
+ * @param {string|null} [options.transferId] el folio de un intento anterior: se sigue desde donde se quedó
118
+ * @param {(transferId: string) => void} [options.onTransferId] el folio nuevo, para retomar si algo falla
119
+ * @param {(loaded: number, total: number) => void} [options.onProgress] lo honesto (ver honestLoadedOf), en bytes
120
+ * @param {boolean} [options.holdBeforeFull] true (por omisión): la barra se queda en 99 % hasta que el servidor confirma el
121
+ * último trozo (una vista sin «Procesando»); false: llega a 100 % cuando salió el último byte
122
+ * @param {UploadSendRequest} options.sendRequest
123
+ * @returns {{done: Promise<string>, abort: () => void}}
124
+ */
125
+ export declare function startChunkedUpload(options: {
126
+ file: Blob;
127
+ fileName: string;
128
+ fieldName: string;
129
+ metadata?: object | null;
130
+ url: string;
131
+ patchUrl: string;
132
+ headers?: Record<string, unknown>;
133
+ withCredentials?: boolean;
134
+ chunkSize: number;
135
+ retryDelays?: number[] | null;
136
+ transferId?: string | null;
137
+ onTransferId?: (transferId: string) => void;
138
+ onProgress?: (loaded: number, total: number) => void;
139
+ holdBeforeFull?: boolean;
140
+ sendRequest: UploadSendRequest;
141
+ }): {
142
+ done: Promise<string>;
143
+ abort: () => void;
144
+ };
145
+ /**
146
+ * La subida por trozos para el cargador mini (upload de FileUploadInput y FormFileUploadField) o para una pantalla propia:
147
+ * regresa esa función upload. Todo va por trozos (un archivo chico va en un solo trozo); si solo los grandes deben ir por
148
+ * trozos, la pantalla elige: upload={(file, options) => (file.size > LIMITE ? porTrozos : subidaNormal)(file, options)}.
149
+ *
150
+ * const uploadBackup = createChunkedUpload({url: '/upload', patchUrl: '/upload?patch=', chunkSize: '1 MB'});
151
+ * <FileUploadInput label="Respaldo" value={fileId} onChange={setFileId} upload={uploadBackup} />
152
+ *
153
+ * Al terminar regresa {id: folio} (idOf lo lee por omisión). Con todo enviado la barra llega a 100 % y el renglón dice
154
+ * «Procesando» hasta que el servidor guarda el último trozo. Los errores son ApiError: el renglón enseña el motivo del
155
+ * servidor (con toProblem, también el de un backend que no contesta RFC 9457). Cancelar termina en AbortError.
156
+ *
157
+ * @param {object} options
158
+ * @param {string} options.url a dónde va el POST que pide el folio
159
+ * @param {string} [options.patchUrl] a dónde va cada trozo, con el folio pegado al final; por omisión url + '?patch='
160
+ * @param {number|string} [options.chunkSize] en bytes o con unidades ('1 MB'); por omisión 5 MB, el de FilePond
161
+ * @param {number[]|null} [options.chunkRetryDelays] esperas entre reintentos de un trozo (ms); null o [] = sin reintentos
162
+ * @param {Record<string, string>} [options.headers] van en todas las peticiones, como texto normal (viajan en UTF-8)
163
+ * @param {boolean} [options.withCredentials]
164
+ * @param {(body: any, response: Response) => any} [options.toProblem] el error del backend a la forma RFC 9457
165
+ * @param {UploadSendRequest} [options.sendRequest] cómo viaja cada petición; por omisión uploadWithProgress de la casa
166
+ * @param {string} [options.fieldName] el campo del primer POST; por omisión 'file'
167
+ * @param {object|null} [options.metadata] lo que viaja en ese campo en JSON, como los metadatos de FilePond
168
+ * @returns {(file: File, options?: {signal?: AbortSignal, onUploadProgress?: (transfer: {loaded: number, total: number}) => void}) => Promise<{id: string}>}
169
+ */
170
+ export declare function createChunkedUpload({ url, patchUrl, chunkSize, chunkRetryDelays, headers, withCredentials, toProblem, sendRequest, fieldName, metadata }: {
171
+ url: string;
172
+ patchUrl?: string;
173
+ chunkSize?: number | string;
174
+ chunkRetryDelays?: number[] | null;
175
+ headers?: Record<string, string>;
176
+ withCredentials?: boolean;
177
+ toProblem?: (body: any, response: Response) => any;
178
+ sendRequest?: UploadSendRequest;
179
+ fieldName?: string;
180
+ metadata?: object | null;
181
+ }): (file: File, options?: {
182
+ signal?: AbortSignal;
183
+ onUploadProgress?: (transfer: {
184
+ loaded: number;
185
+ total: number;
186
+ }) => void;
187
+ }) => Promise<{
188
+ id: string;
189
+ }>;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @param {string} url
3
- * @param {FormData} formData los archivos y los campos que pida el backend
3
+ * @param {FormData|Blob|null} formData los archivos y los campos que pida el backend (un trozo del archivo, o nada en un HEAD)
4
4
  * @param {object} [options]
5
+ * @param {string} [options.method] por omisión POST
5
6
  * @param {Record<string, string>} [options.headers] p. ej. Authorization; Accept-Language ya va con el idioma activo
6
7
  * @param {(percent: number, transfer: {loaded: number, total: number}) => void} [options.onProgress] 0 a 100 de lo que ya
7
8
  * viajó del cuerpo, y los bytes (para la velocidad y lo que falta)
@@ -9,7 +10,8 @@
9
10
  * @param {boolean} [options.withCredentials] manda las cookies a otro dominio (sesión de Sanctum en otro subdominio)
10
11
  * @returns {Promise<Response>}
11
12
  */
12
- export declare function uploadWithProgress(url: string, formData: FormData, { headers, onProgress, signal, withCredentials }?: {
13
+ export declare function uploadWithProgress(url: string, formData: FormData | Blob | null, { method, headers, onProgress, signal, withCredentials }?: {
14
+ method?: string;
13
15
  headers?: Record<string, string>;
14
16
  onProgress?: (percent: number, transfer: {
15
17
  loaded: number;
@@ -42,6 +42,7 @@ function AttachmentsInputContent(props) {
42
42
  showButton = true,
43
43
  pickerRef,
44
44
  disabled = false,
45
+ required = false,
45
46
  busy = false,
46
47
  onReject,
47
48
  template,
@@ -121,7 +122,7 @@ function AttachmentsInputContent(props) {
121
122
  ]
122
123
  }
123
124
  );
124
- return label ? /* @__PURE__ */ jsx(FieldFrame, { inputId: buttonId, label, children: content }) : content;
125
+ return label ? /* @__PURE__ */ jsx(FieldFrame, { inputId: buttonId, label, required, children: content }) : content;
125
126
  }
126
127
  const AttachmentsInput = withPieceSlots(AttachmentsInputContent, THEME_NAME);
127
128
  export {
@@ -1 +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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} 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 {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n * @param {Record<string, import('react').ElementType>} [props.slots] otro componente en lugar de una parte (root, actions, list…)\n * @param {Record<string, object>} [props.slotProps] props de más para una parte\n */\nfunction AttachmentsInputContent(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, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const buttonId = `${name ?? 'attachments'}-${generatedId}`;\n const errorText = validationTextOf(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',\n }}\n sx={sx}>\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\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\n// OJO: con su tipo propio, el genérico (T: el tipo de cada archivo) que la envoltura borraría; slots y slotProps van en su\n// documentación\n/** @type {typeof AttachmentsInputContent} */\nconst AttachmentsInput = withPieceSlots(AttachmentsInputContent, THEME_NAME);\nexport default AttachmentsInput;\n"],"names":[],"mappings":"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"}
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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} 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.required] obligatorio: el asterisco en la etiqueta (lo revisa el esquema del formulario)\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 {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n * @param {Record<string, import('react').ElementType>} [props.slots] otro componente en lugar de una parte (root, actions, list…)\n * @param {Record<string, object>} [props.slotProps] props de más para una parte\n */\nfunction AttachmentsInputContent(props) {\n const {\n name, label, value = [], onChange, onBlur, onRemove, accept, maxSize, maxFiles, maxTotalSize, error, helperText, buttonLabel,\n showButton = true, pickerRef, disabled = false, required = false, busy = false, onReject, template, actions, sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const buttonId = `${name ?? 'attachments'}-${generatedId}`;\n const errorText = validationTextOf(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',\n }}\n sx={sx}>\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} required={required}>{content}</FieldFrame> : content;\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\n// OJO: con su tipo propio, el genérico (T: el tipo de cada archivo) que la envoltura borraría; slots y slotProps van en su\n// documentación\n/** @type {typeof AttachmentsInputContent} */\nconst AttachmentsInput = withPieceSlots(AttachmentsInputContent, THEME_NAME);\nexport default AttachmentsInput;\n"],"names":[],"mappings":"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"}
@@ -34,6 +34,7 @@ function FileInputContent(props) {
34
34
  buttonLabel,
35
35
  emptyText,
36
36
  disabled = false,
37
+ required = false,
37
38
  busy = false,
38
39
  onReject,
39
40
  template,
@@ -55,7 +56,7 @@ function FileInputContent(props) {
55
56
  onBlur?.();
56
57
  };
57
58
  const picker = useFilePicker({ accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts });
58
- return /* @__PURE__ */ jsx(FieldFrame, { inputId: buttonId, label, children: /* @__PURE__ */ jsxs("div", { children: [
59
+ return /* @__PURE__ */ jsx(FieldFrame, { inputId: buttonId, label, required, children: /* @__PURE__ */ jsxs("div", { children: [
59
60
  /* @__PURE__ */ jsxs(
60
61
  FileInputRoot,
61
62
  {
@@ -1 +1 @@
1
- {"version":3,"file":"FileInput.js","sources":["../../src/upload/FileInput.jsx"],"sourcesContent":["// Un archivo del tamaño de un campo, para un formulario (la constancia fiscal de un cliente, el contrato firmado): la\n// etiqueta como los demás campos, el botón «Elegir archivo», el elegido en su chip con la × y abajo su ayuda o su error.\n// Dentro de una FormSection se vuelve renglón, como los demás campos. También se puede soltar el archivo encima.\n//\n// Solo ELIGE (controlado: value / onChange), con las mismas reglas que la zona grande (accept, maxSize; useFilePicker):\n// enviarlo es de la pantalla. value puede ser un archivo nuevo (File) o uno que ya está guardado ({name, size?}), para\n// editar un registro que ya lo tiene.\n//\n// Huecos: template («Descargar la plantilla» junto al botón), actions (botones suyos en el mismo renglón), emptyText,\n// buttonLabel y texts. Con formik: FormFileField (/forms); con react-hook-form, el del mismo nombre (/react-hook-form).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFileInput con defaultProps y\n// styleOverrides de sus partes: root, button, fileChip, empty.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.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.CalcitaFileInput)\nconst THEME_NAME = 'CalcitaFileInput';\nconst FileInputRoot = piecePart(THEME_NAME, 'Root', Stack);\nconst FileInputButton = piecePart(THEME_NAME, 'Button', Button);\nconst FileInputChip = piecePart(THEME_NAME, 'FileChip', Chip);\nconst FileInputEmpty = piecePart(THEME_NAME, 'Empty', Typography);\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida\n * @param {File|{name: string, size?: number}|null} [props.value] el archivo nuevo o el que ya está guardado\n * @param {(file: File|null) => void} props.onChange null al quitarlo\n * @param {() => void} [props.onBlur] se llama al elegir o quitar (para que el formulario lo marque como tocado)\n * @param {string|string[]} [props.accept] 'excel' | ['pdf', 'image'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [props.maxSize] bytes o '10 MB'\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 «Elegir archivo»\n * @param {string} [props.emptyText] ya traducido; por omisión «Ningún archivo elegido»\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.busy] candado mientras se sube: no se puede elegir, soltar 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 al botón\n * @param {import('react').ReactNode} [props.actions] botones suyos en el mismo renglón\n * @param {object} [props.sx] ajustes del renglón\n * @param {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction FileInputContent(props) {\n const {\n name, label, value = null, onChange, onBlur, accept, maxSize, error, helperText = ' ', buttonLabel, emptyText, disabled = false,\n busy = false, onReject, template, actions, sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const generatedId = useId();\n const buttonId = `${name ?? 'file'}-${generatedId}`;\n const errorText = validationTextOf(error);\n const isLocked = disabled || busy;\n const change = (file) => {\n onChange(file);\n onBlur?.();\n };\n const picker = useFilePicker({accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts});\n\n return (\n <FieldFrame inputId={buttonId} label={label}>\n <div>\n <FileInputRoot\n direction=\"row\"\n aria-busy={busy}\n {...picker.dropHandlers}\n ownSx={{\n alignItems: 'center', flexWrap: 'wrap', gap: 1, minHeight: 40, borderRadius: 1, px: 0.5, mx: -0.5,\n outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main',\n bgcolor: picker.isDragging ? 'action.hover' : 'transparent',\n }}\n sx={sx}\n >\n <input {...picker.inputProps} name={name} aria-hidden=\"true\" tabIndex={-1} />\n <FileInputButton id={buttonId} variant=\"outlined\" size=\"small\" startIcon={<AttachFileRoundedIcon />} onClick={picker.open}\n disabled={isLocked} ownSx={{flexShrink: 0}}>\n {buttonLabel ?? t('upload.choose')}\n </FileInputButton>\n {value ? (\n <FileChip file={value} ChipComponent={FileInputChip} removeLabel={t('upload.remove', {name: value.name})}\n onRemove={isLocked ? undefined : () => change(null)} />\n ) : (\n <FileInputEmpty variant=\"body2\" color=\"text.secondary\">{emptyText ?? t('upload.empty')}</FileInputEmpty>\n )}\n {template && <TemplateDownloadButton size=\"small\" texts={texts} {...template} />}\n {actions}\n </FileInputRoot>\n <FormHelperText error={Boolean(errorText)}>{errorText ?? helperText}</FormHelperText>\n </div>\n </FieldFrame>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileInput = withPieceSlots(FileInputContent, THEME_NAME);\nexport default FileInput;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAiCA,MAAM,aAAa;AACnB,MAAM,gBAAgB,UAAU,YAAY,QAAQ,KAAK;AACzD,MAAM,kBAAkB,UAAU,YAAY,UAAU,MAAM;AAC9D,MAAM,gBAAgB,UAAU,YAAY,YAAY,IAAI;AAC5D,MAAM,iBAAiB,UAAU,YAAY,SAAS,UAAU;AA0BhE,SAAS,iBAAiB,OAAO;AAC7B,QAAM;AAAA,IACF;AAAA,IAAM;AAAA,IAAO,QAAQ;AAAA,IAAM;AAAA,IAAU;AAAA,IAAQ;AAAA,IAAQ;AAAA,IAAS;AAAA,IAAO,aAAa;AAAA,IAAK;AAAA,IAAa;AAAA,IAAW,WAAW;AAAA,IAC1H,OAAO;AAAA,IAAO;AAAA,IAAU;AAAA,IAAU;AAAA,IAAS;AAAA,IAAI;AAAA,IAAU;AAAA,IACzD,GAAG;AAAA,EAAA,IACH,cAAc,OAAO,UAAU;AACnC,mBAAiB,YAAY,YAAY;AACzC,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AAEjD,QAAM,mBAAmB,kBAAkB,EAAC,MAAM,OAAO,UAAS;AAClE,QAAM,cAAc,MAAA;AACpB,QAAM,WAAW,GAAG,QAAQ,MAAM,IAAI,WAAW;AACjD,QAAM,YAAY,iBAAiB,KAAK;AACxC,QAAM,WAAW,YAAY;AAC7B,QAAM,SAAS,CAAC,SAAS;AACrB,aAAS,IAAI;AACb,aAAA;AAAA,EACJ;AACA,QAAM,SAAS,cAAc,EAAC,QAAQ,SAAS,UAAU,UAAU,UAAU,QAAQ,UAAU,MAAA,CAAM;AAErG,6BACK,YAAA,EAAW,SAAS,UAAU,OAC3B,+BAAC,OAAA,EACG,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,WAAU;AAAA,QACV,aAAW;AAAA,QACV,GAAG,OAAO;AAAA,QACX,OAAO;AAAA,UACH,YAAY;AAAA,UAAU,UAAU;AAAA,UAAQ,KAAK;AAAA,UAAG,WAAW;AAAA,UAAI,cAAc;AAAA,UAAG,IAAI;AAAA,UAAK,IAAI;AAAA,UAC7F,SAAS,OAAO,aAAa,IAAI;AAAA,UAAG,cAAc;AAAA,UAAU,cAAc;AAAA,UAC1E,SAAS,OAAO,aAAa,iBAAiB;AAAA,QAAA;AAAA,QAElD;AAAA,QAEA,UAAA;AAAA,UAAA,oBAAC,SAAA,EAAO,GAAG,OAAO,YAAY,MAAY,eAAY,QAAO,UAAU,IAAI;AAAA,UAC3E;AAAA,YAAC;AAAA,YAAA;AAAA,cAAgB,IAAI;AAAA,cAAU,SAAQ;AAAA,cAAW,MAAK;AAAA,cAAQ,+BAAY,uBAAA,EAAsB;AAAA,cAAI,SAAS,OAAO;AAAA,cACpG,UAAU;AAAA,cAAU,OAAO,EAAC,YAAY,EAAA;AAAA,cACpD,UAAA,eAAe,EAAE,eAAe;AAAA,YAAA;AAAA,UAAA;AAAA,UAEpC,QACG;AAAA,YAAC;AAAA,YAAA;AAAA,cAAS,MAAM;AAAA,cAAO,eAAe;AAAA,cAAe,aAAa,EAAE,iBAAiB,EAAC,MAAM,MAAM,MAAK;AAAA,cAC7F,UAAU,WAAW,SAAY,MAAM,OAAO,IAAI;AAAA,YAAA;AAAA,UAAA,IAE5D,oBAAC,gBAAA,EAAe,SAAQ,SAAQ,OAAM,kBAAkB,UAAA,aAAa,EAAE,cAAc,EAAA,CAAE;AAAA,UAE1F,YAAY,oBAAC,wBAAA,EAAuB,MAAK,SAAQ,OAAe,GAAG,UAAU;AAAA,UAC7E;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,wBAEJ,gBAAA,EAAe,OAAO,QAAQ,SAAS,GAAI,uBAAa,WAAA,CAAW;AAAA,EAAA,EAAA,CACxE,EAAA,CACJ;AAER;AAGA,MAAM,YAAY,eAAe,kBAAkB,UAAU;"}
1
+ {"version":3,"file":"FileInput.js","sources":["../../src/upload/FileInput.jsx"],"sourcesContent":["// Un archivo del tamaño de un campo, para un formulario (la constancia fiscal de un cliente, el contrato firmado): la\n// etiqueta como los demás campos, el botón «Elegir archivo», el elegido en su chip con la × y abajo su ayuda o su error.\n// Dentro de una FormSection se vuelve renglón, como los demás campos. También se puede soltar el archivo encima.\n//\n// Solo ELIGE (controlado: value / onChange), con las mismas reglas que la zona grande (accept, maxSize; useFilePicker):\n// enviarlo es de la pantalla. value puede ser un archivo nuevo (File) o uno que ya está guardado ({name, size?}), para\n// editar un registro que ya lo tiene.\n//\n// Huecos: template («Descargar la plantilla» junto al botón), actions (botones suyos en el mismo renglón), emptyText,\n// buttonLabel y texts. Con formik: FormFileField (/forms); con react-hook-form, el del mismo nombre (/react-hook-form).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFileInput con defaultProps y\n// styleOverrides de sus partes: root, button, fileChip, empty.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.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.CalcitaFileInput)\nconst THEME_NAME = 'CalcitaFileInput';\nconst FileInputRoot = piecePart(THEME_NAME, 'Root', Stack);\nconst FileInputButton = piecePart(THEME_NAME, 'Button', Button);\nconst FileInputChip = piecePart(THEME_NAME, 'FileChip', Chip);\nconst FileInputEmpty = piecePart(THEME_NAME, 'Empty', Typography);\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida\n * @param {File|{name: string, size?: number}|null} [props.value] el archivo nuevo o el que ya está guardado\n * @param {(file: File|null) => void} props.onChange null al quitarlo\n * @param {() => void} [props.onBlur] se llama al elegir o quitar (para que el formulario lo marque como tocado)\n * @param {string|string[]} [props.accept] 'excel' | ['pdf', 'image'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [props.maxSize] bytes o '10 MB'\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 «Elegir archivo»\n * @param {string} [props.emptyText] ya traducido; por omisión «Ningún archivo elegido»\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.required] obligatorio: el asterisco en la etiqueta (lo revisa el esquema del formulario)\n * @param {boolean} [props.busy] candado mientras se sube: no se puede elegir, soltar 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 al botón\n * @param {import('react').ReactNode} [props.actions] botones suyos en el mismo renglón\n * @param {object} [props.sx] ajustes del renglón\n * @param {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction FileInputContent(props) {\n const {\n name, label, value = null, onChange, onBlur, accept, maxSize, error, helperText = ' ', buttonLabel, emptyText, disabled = false, required = false,\n busy = false, onReject, template, actions, sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const generatedId = useId();\n const buttonId = `${name ?? 'file'}-${generatedId}`;\n const errorText = validationTextOf(error);\n const isLocked = disabled || busy;\n const change = (file) => {\n onChange(file);\n onBlur?.();\n };\n const picker = useFilePicker({accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts});\n\n return (\n <FieldFrame inputId={buttonId} label={label} required={required}>\n <div>\n <FileInputRoot\n direction=\"row\"\n aria-busy={busy}\n {...picker.dropHandlers}\n ownSx={{\n alignItems: 'center', flexWrap: 'wrap', gap: 1, minHeight: 40, borderRadius: 1, px: 0.5, mx: -0.5,\n outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main',\n bgcolor: picker.isDragging ? 'action.hover' : 'transparent',\n }}\n sx={sx}\n >\n <input {...picker.inputProps} name={name} aria-hidden=\"true\" tabIndex={-1} />\n <FileInputButton id={buttonId} variant=\"outlined\" size=\"small\" startIcon={<AttachFileRoundedIcon />} onClick={picker.open}\n disabled={isLocked} ownSx={{flexShrink: 0}}>\n {buttonLabel ?? t('upload.choose')}\n </FileInputButton>\n {value ? (\n <FileChip file={value} ChipComponent={FileInputChip} removeLabel={t('upload.remove', {name: value.name})}\n onRemove={isLocked ? undefined : () => change(null)} />\n ) : (\n <FileInputEmpty variant=\"body2\" color=\"text.secondary\">{emptyText ?? t('upload.empty')}</FileInputEmpty>\n )}\n {template && <TemplateDownloadButton size=\"small\" texts={texts} {...template} />}\n {actions}\n </FileInputRoot>\n <FormHelperText error={Boolean(errorText)}>{errorText ?? helperText}</FormHelperText>\n </div>\n </FieldFrame>\n );\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileInput = withPieceSlots(FileInputContent, THEME_NAME);\nexport default FileInput;\n"],"names":[],"mappings":"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"}
@@ -73,6 +73,7 @@ function FileUploadInputContent(props) {
73
73
  error,
74
74
  helperText = " ",
75
75
  disabled = false,
76
+ required = false,
76
77
  actions,
77
78
  onUploaded,
78
79
  onUploadFailed,
@@ -278,7 +279,7 @@ function FileUploadInputContent(props) {
278
279
  if (!label) {
279
280
  return row;
280
281
  }
281
- return /* @__PURE__ */ jsx(FieldFrame, { inputId, labelId, label, children: row });
282
+ return /* @__PURE__ */ jsx(FieldFrame, { inputId, labelId, label, required, children: row });
282
283
  }
283
284
  const FileUploadInput = withPieceSlots(FileUploadInputContent, THEME_NAME);
284
285
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadInput.js","sources":["../../src/upload/FileUploadInput.jsx"],"sourcesContent":["// El cargador MINI: un archivo que se SUBE en cuanto se elige, del tamaño de un renglón. Para una lista de documentos (el\n// expediente de un cliente, lo que pide una solicitud): a la izquierda el nombre del documento (la etiqueta del renglón) y a\n// la derecha el nombre del archivo, su estado y el botón.\n//\n// estado Por cargar · Subiendo 40 % · Procesando · Cargado · No se cargó\n// botón Cargar (lleno) → ya cargado, Sustituir (con borde); mientras sube, Cancelar\n// íconos bajar el que ya subió (si la pantalla da download) y quitarlo\n// abajo el avance en palabras («1.2 MB de 3 MB · 350 kB/s · faltan 5 segundos»; el porcentaje ya va en el estado) o el\n// motivo del error\n//\n// Al formulario llega SOLO lo que el servidor da para reconocer el archivo (su id, por omisión result.id): el formulario\n// manda ese id junto con lo demás y el backend lo liga a su registro. Mientras sube, el valor es '' (no se puede guardar\n// con el id del anterior). Subir es de la pantalla: `upload` recibe el archivo con su señal para cancelar, su avance y una\n// llave por archivo elegido (si la red se cae a media subida, el servidor no deja dos copias). Con uploadWithProgress o\n// con axios, igual.\n//\n// Lo que es del negocio (Aprobado, Falta información) no va aquí: la pantalla lo pinta junto, con `actions`, y sabe en\n// qué va cada uno con onStatusChange (para «3 de 5 cargados»). Tipo y peso se revisan ANTES de subir (accept, maxSize): no\n// se gastan minutos subiendo lo que el servidor va a rechazar; el motivo sale abajo, en rojo.\n//\n// Dentro de una FormSection se vuelve renglón, como los demás campos. También se puede soltar el archivo encima.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFileUploadInput con defaultProps y\n// styleOverrides de sus partes: root, fileName, status, button, helper.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useId, useRef, useState} from 'react';\nimport {useMachine} from '@xstate/react';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport FormHelperText from '@mui/material/FormHelperText';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';\nimport CloseRoundedIcon from '@mui/icons-material/CloseRounded';\nimport FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';\nimport FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';\n\nimport {startDownload} from '@/downloads/downloadQueue.js';\nimport {useValidationText} from '@/forms/useValidationText.js';\nimport {newIdentifier} from '@/identifiers.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {fieldErrorsOf} from '@/problems.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {FILE_REJECTION} from '@/upload/fileRules.js';\nimport {transferProgressOf} from '@/upload/transferProgress.js';\nimport {buildUploadActors, uploadMachine} from '@/upload/uploadMachine.js';\nimport {useFilePicker} from '@/upload/useFilePicker.js';\n\n/** En qué va el archivo de un renglón (onStatusChange). */\nexport const FILE_UPLOAD_STATUS = Object.freeze({\n pending: 'pending', uploading: 'uploading', processing: 'processing', uploaded: 'uploaded', failed: 'failed',\n});\n\n// el color de la etiqueta de cada estado (el color es información)\nconst STATUS_COLORS = Object.freeze({\n [FILE_UPLOAD_STATUS.pending]: 'default',\n [FILE_UPLOAD_STATUS.uploading]: 'info',\n [FILE_UPLOAD_STATUS.processing]: 'info',\n [FILE_UPLOAD_STATUS.uploaded]: 'success',\n [FILE_UPLOAD_STATUS.failed]: 'error',\n});\n\n// entre las partes del avance en palabras\nconst PROGRESS_SEPARATOR = ' · ';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaFileUploadInput)\nconst THEME_NAME = 'CalcitaFileUploadInput';\nconst FileUploadRoot = piecePart(THEME_NAME, 'Root', Stack);\nconst FileUploadName = piecePart(THEME_NAME, 'FileName', Typography);\nconst FileUploadStatus = piecePart(THEME_NAME, 'Status', Chip);\nconst FileUploadButton = piecePart(THEME_NAME, 'Button', Button);\nconst FileUploadHelper = piecePart(THEME_NAME, 'Helper', FormHelperText);\n\n/**\n * El texto de un error de la subida: el del campo si el servidor mandó un 422 con su lista, si no su detail, y si no hubo\n * respuesta, el genérico.\n *\n * @param {unknown} error\n * @returns {string|null}\n */\nfunction uploadErrorTextOf(error) {\n const problem = /** @type {{problem?: object, message?: string}} */ (error)?.problem;\n const firstFieldMessage = Object.values(fieldErrorsOf(problem ?? null)).flat()[0];\n return firstFieldMessage ?? problem?.detail ?? null;\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida: el nombre del documento; sin ella, el renglón va solo\n * @param {string|null} [props.value] lo que el servidor dio para reconocer el archivo subido ('' o null = ninguno)\n * @param {(value: string) => void} props.onChange recibe el id al terminar de subir, '' al quitarlo o mientras sube otro\n * @param {() => void} [props.onBlur] se llama al elegir o quitar (para que el formulario lo marque como tocado)\n * @param {(file: File, options: {signal: AbortSignal, idempotencyKey: string, onUploadProgress: (transfer: {loaded: number, total: number, rate?: number, estimated?: number}) => void}) => Promise<any>} props.upload\n * sube el archivo (con uploadWithProgress, axios o el cliente del proyecto) y regresa lo que conteste el servidor\n * @param {(result: any) => string} [props.idOf] de la respuesta, lo que se guarda en el formulario; por omisión result.id\n * @param {{name: string, size?: number}|null} [props.uploadedFile] el que ya estaba subido, para enseñar su nombre al editar\n * @param {(value: string, transferOptions: object) => Promise<{blob: Blob, fileName: string}>} [props.download] baja el\n * que ya subió (pasa por el panel de descargas); sin él no sale el ícono\n * @param {string|string[]} [props.accept] 'pdf' | ['pdf', 'image'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [props.maxSize] bytes o '10 MB'\n * @param {string} [props.limitsText] ya traducido: qué acepta, en palabras («PDF o foto, hasta 5 MB»); sale al pasar sobre\n * el botón. Por omisión solo el peso máximo\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] ya traducida\n * @param {boolean} [props.disabled]\n * @param {import('react').ReactNode} [props.actions] lo del negocio junto al estado (Aprobado, Falta información)\n * @param {(result: any) => void} [props.onUploaded] terminó de subir (p. ej. para enseñar el folio de la respuesta)\n * @param {(error: unknown) => void} [props.onUploadFailed] no se pudo subir (el motivo ya sale abajo)\n * @param {() => void} [props.onCancel] la persona canceló a media subida\n * @param {() => void} [props.onRemove] la persona quitó el archivo\n * @param {(status: string) => void} [props.onStatusChange] uno de FILE_UPLOAD_STATUS, cada vez que cambia\n * @param {object} [props.sx] ajustes del renglón\n * @param {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction FileUploadInputContent(props) {\n const {\n name, label, value = '', onChange, onBlur, upload, idOf = (result) => result?.id, uploadedFile = null, download, accept, maxSize,\n limitsText, error, helperText = ' ', disabled = false, actions, onUploaded, onUploadFailed, onCancel, onRemove, onStatusChange,\n sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const inputId = `${name ?? 'file-upload'}-${generatedId}`;\n const labelId = `${inputId}-label`;\n const buttonTextId = `${inputId}-button`;\n const [chosenFile, setChosenFile] = useState(null);\n const [rejectedText, setRejectedText] = useState(null);\n const startedAtRef = useRef(0);\n const [uploadSnapshot, sendToUpload] = useMachine(uploadMachine.provide({\n actors: buildUploadActors(({file, idempotencyKey}, onProgress, signal) => upload(file, {\n signal,\n idempotencyKey,\n onUploadProgress: (transfer) => {\n const total = transfer.total ?? file.size;\n onProgress(total ? Math.round((transfer.loaded * 100) / total) : 0, {...transfer, total});\n },\n })),\n }));\n const {progress, transfer, result, problem} = uploadSnapshot.context;\n const isSending = uploadSnapshot.matches({uploading: 'sending'});\n const isProcessing = uploadSnapshot.matches({uploading: 'processing'});\n const isUploading = isSending || isProcessing;\n const isFailed = uploadSnapshot.matches('failed') || Boolean(rejectedText);\n const isUploaded = Boolean(value) && !isUploading && !isFailed;\n let status = FILE_UPLOAD_STATUS.pending;\n if (isSending) {\n status = FILE_UPLOAD_STATUS.uploading;\n } else if (isProcessing) {\n status = FILE_UPLOAD_STATUS.processing;\n } else if (isFailed) {\n status = FILE_UPLOAD_STATUS.failed;\n } else if (isUploaded) {\n status = FILE_UPLOAD_STATUS.uploaded;\n }\n\n // al terminar, el formulario se queda con el id; quien usa el campo puede enseñar lo demás de la respuesta\n useEffect(() => {\n if (!result) {\n return;\n }\n onChange(idOf(result) ?? '');\n onUploaded?.(result);\n // OJO: solo cuando llega una respuesta nueva; onChange, idOf y onUploaded cambian en cada render\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [result]);\n useEffect(() => {\n if (problem) {\n onUploadFailed?.(problem);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [problem]);\n useEffect(() => {\n onStatusChange?.(status);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n // si el formulario se limpia (después de guardar), el renglón también regresa a «por cargar»\n useEffect(() => {\n if (!value && uploadSnapshot.matches('done')) {\n setChosenFile(null);\n sendToUpload({type: 'RESET'});\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n const startUpload = (file) => {\n onBlur?.();\n setRejectedText(null);\n setChosenFile(file);\n // mientras sube, el formulario no tiene archivo: no se puede guardar con el id del anterior\n onChange('');\n startedAtRef.current = Date.now();\n sendToUpload({type: 'UPLOAD', input: {file, idempotencyKey: newIdentifier()}});\n };\n const picker = useFilePicker({\n accept, maxSize, disabled: disabled || isUploading, onAccept: startUpload, texts,\n onReject: (file, reason) => {\n onBlur?.();\n setRejectedText(reason === FILE_REJECTION.size\n ? t('upload.tooLarge', {name: file.name, max: formatFileSize(picker.maxBytes)})\n : t('upload.invalidType', {name: file.name, accept: picker.resolvedAccept}));\n },\n });\n const remove = () => {\n setChosenFile(null);\n setRejectedText(null);\n onChange('');\n onBlur?.();\n sendToUpload({type: 'RESET'});\n onRemove?.();\n };\n const cancel = () => {\n sendToUpload({type: 'CANCEL'});\n onCancel?.();\n };\n\n const shownFile = chosenFile ?? (value ? uploadedFile : null);\n const statusLabel = status === FILE_UPLOAD_STATUS.uploading\n ? t('upload.status.uploading', {percent: progress ?? 0})\n : t(`upload.status.${status}`);\n const validationText = validationTextOf(error);\n const errorText = rejectedText\n ?? (uploadSnapshot.matches('failed') ? uploadErrorTextOf(problem) ?? t('upload.failed') : null)\n ?? (isUploading ? null : validationText);\n let progressText = null;\n if (isProcessing) {\n progressText = t('upload.processing');\n } else if (isSending) {\n // «1.2 MB de 3 MB · 350 kB/s · faltan 5 segundos»: la velocidad y lo que falta, cuando ya se pueden decir (el\n // porcentaje ya va en la etiqueta del estado)\n const now = transfer ? transferProgressOf(transfer, startedAtRef.current) : null;\n progressText = now ? [\n t('upload.amount', {loaded: formatFileSize(now.loaded), total: formatFileSize(now.total)}),\n now.bytesPerSecond ? t('upload.speed', {size: formatFileSize(now.bytesPerSecond)}) : null,\n now.remainingSeconds != null ? t('upload.remaining', {count: now.remainingSeconds}) : null,\n ].filter(Boolean).join(PROGRESS_SEPARATOR) : t('upload.starting');\n } else if (uploadSnapshot.matches('cancelled')) {\n progressText = t('upload.cancelled');\n }\n const buttonHint = limitsText ?? (picker.maxBytes ? t('upload.maxSizeHint', {max: formatFileSize(picker.maxBytes)}) : '');\n const downloadUploaded = () => startDownload({\n label: shownFile?.name ?? t('upload.download'),\n request: (transferOptions) => download(value, transferOptions),\n });\n\n const row = (\n <div>\n <FileUploadRoot\n direction=\"row\"\n aria-busy={isUploading}\n {...picker.dropHandlers}\n ownSx={{\n // en el teléfono el nombre baja a su renglón; en escritorio se corta con «…» y el botón no se mueve\n alignItems: 'center', justifyContent: 'flex-end', flexWrap: {xs: 'wrap', sm: 'nowrap'}, gap: 1, minWidth: 0, minHeight: 40,\n px: 0.5, mx: -0.5, borderRadius: 1,\n outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main',\n bgcolor: picker.isDragging ? 'action.hover' : 'transparent',\n }}\n sx={sx}\n >\n <input {...picker.inputProps} id={inputId} name={name} aria-labelledby={label ? labelId : undefined} />\n {shownFile && (\n // el nombre cortado si es largo y completo al pasar el cursor; en el teléfono baja a su renglón\n <Tooltip title={shownFile.size != null ? `${shownFile.name} · ${formatFileSize(shownFile.size)}` : shownFile.name}>\n <FileUploadName variant=\"caption\" color=\"text.secondary\" noWrap\n ownSx={{minWidth: 0, flexShrink: 1, flexBasis: {xs: '100%', sm: 'auto'}, textAlign: {xs: 'right', sm: 'left'}}}>\n {shownFile.name}\n </FileUploadName>\n </Tooltip>\n )}\n {actions}\n <FileUploadStatus\n size=\"small\"\n color={STATUS_COLORS[status]}\n variant={status === FILE_UPLOAD_STATUS.pending ? 'filled' : 'outlined'}\n label={statusLabel}\n icon={(isUploading && (\n <CircularProgress size={12} color=\"inherit\" variant={isProcessing ? 'indeterminate' : 'determinate'} value={progress ?? 0} />\n )) || (isUploaded && <CheckCircleRoundedIcon />) || undefined}\n ownSx={{flexShrink: 0}}\n />\n {isUploading ? (\n <FileUploadButton size=\"small\" color=\"inherit\" onClick={cancel} ownSx={{flexShrink: 0}}>{t('upload.cancel')}</FileUploadButton>\n ) : (\n // describeChild: lo que acepta se lee como descripción; el nombre sigue siendo «Cargar» y el documento\n <Tooltip title={buttonHint} describeChild>\n <FileUploadButton\n size=\"small\"\n variant={isUploaded ? 'outlined' : 'contained'}\n startIcon={<FileUploadOutlinedIcon />}\n disabled={disabled}\n onClick={picker.open}\n aria-labelledby={label ? `${buttonTextId} ${labelId}` : undefined}\n ownSx={{flexShrink: 0}}\n >\n <span id={buttonTextId}>{isUploaded ? t('upload.replace') : t('upload.upload')}</span>\n </FileUploadButton>\n </Tooltip>\n )}\n {isUploaded && download && (\n <Tooltip title={t('upload.download')}>\n <IconButton size=\"small\" aria-label={t('upload.download')} onClick={downloadUploaded}>\n <FileDownloadOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n {isUploaded && !disabled && (\n <Tooltip title={t('upload.remove', {name: shownFile?.name ?? ''})}>\n <IconButton size=\"small\" aria-label={t('upload.remove', {name: shownFile?.name ?? ''})} onClick={remove}>\n <CloseRoundedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </FileUploadRoot>\n {/* abajo: el error manda; si no, el avance; si no, la ayuda de la pantalla */}\n <FileUploadHelper error={Boolean(errorText)} role={errorText ? 'alert' : (progressText ? 'status' : undefined)}\n ownSx={{textAlign: 'right', mx: 0}}>\n {errorText ?? progressText ?? helperText}\n </FileUploadHelper>\n </div>\n );\n\n // sin etiqueta, el renglón va solo (la pantalla pone el título del documento a su manera)\n if (!label) {\n return row;\n }\n return <FieldFrame inputId={inputId} labelId={labelId} label={label}>{row}</FieldFrame>;\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileUploadInput = withPieceSlots(FileUploadInputContent, THEME_NAME);\nexport default FileUploadInput;\n"],"names":["result","transfer"],"mappings":"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"}
1
+ {"version":3,"file":"FileUploadInput.js","sources":["../../src/upload/FileUploadInput.jsx"],"sourcesContent":["// El cargador MINI: un archivo que se SUBE en cuanto se elige, del tamaño de un renglón. Para una lista de documentos (el\n// expediente de un cliente, lo que pide una solicitud): a la izquierda el nombre del documento (la etiqueta del renglón) y a\n// la derecha el nombre del archivo, su estado y el botón.\n//\n// estado Por cargar · Subiendo 40 % · Procesando · Cargado · No se cargó\n// botón Cargar (lleno) → ya cargado, Sustituir (con borde); mientras sube, Cancelar\n// íconos bajar el que ya subió (si la pantalla da download) y quitarlo\n// abajo el avance en palabras («1.2 MB de 3 MB · 350 kB/s · faltan 5 segundos»; el porcentaje ya va en el estado) o el\n// motivo del error\n//\n// Al formulario llega SOLO lo que el servidor da para reconocer el archivo (su id, por omisión result.id): el formulario\n// manda ese id junto con lo demás y el backend lo liga a su registro. Mientras sube, el valor es '' (no se puede guardar\n// con el id del anterior). Subir es de la pantalla: `upload` recibe el archivo con su señal para cancelar, su avance y una\n// llave por archivo elegido (si la red se cae a media subida, el servidor no deja dos copias). Con uploadWithProgress o\n// con axios, igual.\n//\n// Lo que es del negocio (Aprobado, Falta información) no va aquí: la pantalla lo pinta junto, con `actions`, y sabe en\n// qué va cada uno con onStatusChange (para «3 de 5 cargados»). Tipo y peso se revisan ANTES de subir (accept, maxSize): no\n// se gastan minutos subiendo lo que el servidor va a rechazar; el motivo sale abajo, en rojo.\n//\n// Dentro de una FormSection se vuelve renglón, como los demás campos. También se puede soltar el archivo encima.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaFileUploadInput con defaultProps y\n// styleOverrides de sus partes: root, fileName, status, button, helper.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useId, useRef, useState} from 'react';\nimport {useMachine} from '@xstate/react';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport FormHelperText from '@mui/material/FormHelperText';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';\nimport CloseRoundedIcon from '@mui/icons-material/CloseRounded';\nimport FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';\nimport FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';\n\nimport {startDownload} from '@/downloads/downloadQueue.js';\nimport {useValidationText} from '@/forms/useValidationText.js';\nimport {newIdentifier} from '@/identifiers.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {fieldErrorsOf} from '@/problems.js';\nimport {piecePart, usePieceProps, withPieceSlots} from '@/theme/themePieces.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {warnUnknownProps} from '@/toolkit/unknownProps.js';\nimport {FILE_REJECTION} from '@/upload/fileRules.js';\nimport {transferProgressOf} from '@/upload/transferProgress.js';\nimport {buildUploadActors, uploadMachine} from '@/upload/uploadMachine.js';\nimport {useFilePicker} from '@/upload/useFilePicker.js';\n\n/** En qué va el archivo de un renglón (onStatusChange). */\nexport const FILE_UPLOAD_STATUS = Object.freeze({\n pending: 'pending', uploading: 'uploading', processing: 'processing', uploaded: 'uploaded', failed: 'failed',\n});\n\n// el color de la etiqueta de cada estado (el color es información)\nconst STATUS_COLORS = Object.freeze({\n [FILE_UPLOAD_STATUS.pending]: 'default',\n [FILE_UPLOAD_STATUS.uploading]: 'info',\n [FILE_UPLOAD_STATUS.processing]: 'info',\n [FILE_UPLOAD_STATUS.uploaded]: 'success',\n [FILE_UPLOAD_STATUS.failed]: 'error',\n});\n\n// entre las partes del avance en palabras\nconst PROGRESS_SEPARATOR = ' · ';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaFileUploadInput)\nconst THEME_NAME = 'CalcitaFileUploadInput';\nconst FileUploadRoot = piecePart(THEME_NAME, 'Root', Stack);\nconst FileUploadName = piecePart(THEME_NAME, 'FileName', Typography);\nconst FileUploadStatus = piecePart(THEME_NAME, 'Status', Chip);\nconst FileUploadButton = piecePart(THEME_NAME, 'Button', Button);\nconst FileUploadHelper = piecePart(THEME_NAME, 'Helper', FormHelperText);\n\n/**\n * El texto de un error de la subida: el del campo si el servidor mandó un 422 con su lista, si no su detail, y si no hubo\n * respuesta, el genérico.\n *\n * @param {unknown} error\n * @returns {string|null}\n */\nfunction uploadErrorTextOf(error) {\n const problem = /** @type {{problem?: object, message?: string}} */ (error)?.problem;\n const firstFieldMessage = Object.values(fieldErrorsOf(problem ?? null)).flat()[0];\n return firstFieldMessage ?? problem?.detail ?? null;\n}\n\n/**\n * @param {object} props\n * @param {string} [props.name]\n * @param {import('react').ReactNode} [props.label] ya traducida: el nombre del documento; sin ella, el renglón va solo\n * @param {string|null} [props.value] lo que el servidor dio para reconocer el archivo subido ('' o null = ninguno)\n * @param {(value: string) => void} props.onChange recibe el id al terminar de subir, '' al quitarlo o mientras sube otro\n * @param {() => void} [props.onBlur] se llama al elegir o quitar (para que el formulario lo marque como tocado)\n * @param {(file: File, options: {signal: AbortSignal, idempotencyKey: string, onUploadProgress: (transfer: {loaded: number, total: number, rate?: number, estimated?: number}) => void}) => Promise<any>} props.upload\n * sube el archivo (con uploadWithProgress, axios o el cliente del proyecto) y regresa lo que conteste el servidor\n * @param {(result: any) => string} [props.idOf] de la respuesta, lo que se guarda en el formulario; por omisión result.id\n * @param {{name: string, size?: number}|null} [props.uploadedFile] el que ya estaba subido, para enseñar su nombre al editar\n * @param {(value: string, transferOptions: object) => Promise<{blob: Blob, fileName: string}>} [props.download] baja el\n * que ya subió (pasa por el panel de descargas); sin él no sale el ícono\n * @param {string|string[]} [props.accept] 'pdf' | ['pdf', 'image'] | '.xlsx,.xls' | 'image/*'\n * @param {number|string} [props.maxSize] bytes o '10 MB'\n * @param {string} [props.limitsText] ya traducido: qué acepta, en palabras («PDF o foto, hasta 5 MB»); sale al pasar sobre\n * el botón. Por omisión solo el peso máximo\n * @param {string|{key: string, values?: object}} [props.error]\n * @param {string} [props.helperText] ya traducida\n * @param {boolean} [props.disabled]\n * @param {boolean} [props.required] obligatorio: el asterisco en la etiqueta (lo revisa el esquema del formulario)\n * @param {import('react').ReactNode} [props.actions] lo del negocio junto al estado (Aprobado, Falta información)\n * @param {(result: any) => void} [props.onUploaded] terminó de subir (p. ej. para enseñar el folio de la respuesta)\n * @param {(error: unknown) => void} [props.onUploadFailed] no se pudo subir (el motivo ya sale abajo)\n * @param {() => void} [props.onCancel] la persona canceló a media subida\n * @param {() => void} [props.onRemove] la persona quitó el archivo\n * @param {(status: string) => void} [props.onStatusChange] uno de FILE_UPLOAD_STATUS, cada vez que cambia\n * @param {object} [props.sx] ajustes del renglón\n * @param {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction FileUploadInputContent(props) {\n const {\n name, label, value = '', onChange, onBlur, upload, idOf = (result) => result?.id, uploadedFile = null, download, accept, maxSize,\n limitsText, error, helperText = ' ', disabled = false, required = false, actions, onUploaded, onUploadFailed, onCancel, onRemove, onStatusChange,\n sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const {formatFileSize} = useFormatters();\n const generatedId = useId();\n const inputId = `${name ?? 'file-upload'}-${generatedId}`;\n const labelId = `${inputId}-label`;\n const buttonTextId = `${inputId}-button`;\n const [chosenFile, setChosenFile] = useState(null);\n const [rejectedText, setRejectedText] = useState(null);\n const startedAtRef = useRef(0);\n const [uploadSnapshot, sendToUpload] = useMachine(uploadMachine.provide({\n actors: buildUploadActors(({file, idempotencyKey}, onProgress, signal) => upload(file, {\n signal,\n idempotencyKey,\n onUploadProgress: (transfer) => {\n const total = transfer.total ?? file.size;\n onProgress(total ? Math.round((transfer.loaded * 100) / total) : 0, {...transfer, total});\n },\n })),\n }));\n const {progress, transfer, result, problem} = uploadSnapshot.context;\n const isSending = uploadSnapshot.matches({uploading: 'sending'});\n const isProcessing = uploadSnapshot.matches({uploading: 'processing'});\n const isUploading = isSending || isProcessing;\n const isFailed = uploadSnapshot.matches('failed') || Boolean(rejectedText);\n const isUploaded = Boolean(value) && !isUploading && !isFailed;\n let status = FILE_UPLOAD_STATUS.pending;\n if (isSending) {\n status = FILE_UPLOAD_STATUS.uploading;\n } else if (isProcessing) {\n status = FILE_UPLOAD_STATUS.processing;\n } else if (isFailed) {\n status = FILE_UPLOAD_STATUS.failed;\n } else if (isUploaded) {\n status = FILE_UPLOAD_STATUS.uploaded;\n }\n\n // al terminar, el formulario se queda con el id; quien usa el campo puede enseñar lo demás de la respuesta\n useEffect(() => {\n if (!result) {\n return;\n }\n onChange(idOf(result) ?? '');\n onUploaded?.(result);\n // OJO: solo cuando llega una respuesta nueva; onChange, idOf y onUploaded cambian en cada render\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [result]);\n useEffect(() => {\n if (problem) {\n onUploadFailed?.(problem);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [problem]);\n useEffect(() => {\n onStatusChange?.(status);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n // si el formulario se limpia (después de guardar), el renglón también regresa a «por cargar»\n useEffect(() => {\n if (!value && uploadSnapshot.matches('done')) {\n setChosenFile(null);\n sendToUpload({type: 'RESET'});\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n const startUpload = (file) => {\n onBlur?.();\n setRejectedText(null);\n setChosenFile(file);\n // mientras sube, el formulario no tiene archivo: no se puede guardar con el id del anterior\n onChange('');\n startedAtRef.current = Date.now();\n sendToUpload({type: 'UPLOAD', input: {file, idempotencyKey: newIdentifier()}});\n };\n const picker = useFilePicker({\n accept, maxSize, disabled: disabled || isUploading, onAccept: startUpload, texts,\n onReject: (file, reason) => {\n onBlur?.();\n setRejectedText(reason === FILE_REJECTION.size\n ? t('upload.tooLarge', {name: file.name, max: formatFileSize(picker.maxBytes)})\n : t('upload.invalidType', {name: file.name, accept: picker.resolvedAccept}));\n },\n });\n const remove = () => {\n setChosenFile(null);\n setRejectedText(null);\n onChange('');\n onBlur?.();\n sendToUpload({type: 'RESET'});\n onRemove?.();\n };\n const cancel = () => {\n sendToUpload({type: 'CANCEL'});\n onCancel?.();\n };\n\n const shownFile = chosenFile ?? (value ? uploadedFile : null);\n const statusLabel = status === FILE_UPLOAD_STATUS.uploading\n ? t('upload.status.uploading', {percent: progress ?? 0})\n : t(`upload.status.${status}`);\n const validationText = validationTextOf(error);\n const errorText = rejectedText\n ?? (uploadSnapshot.matches('failed') ? uploadErrorTextOf(problem) ?? t('upload.failed') : null)\n ?? (isUploading ? null : validationText);\n let progressText = null;\n if (isProcessing) {\n progressText = t('upload.processing');\n } else if (isSending) {\n // «1.2 MB de 3 MB · 350 kB/s · faltan 5 segundos»: la velocidad y lo que falta, cuando ya se pueden decir (el\n // porcentaje ya va en la etiqueta del estado)\n const now = transfer ? transferProgressOf(transfer, startedAtRef.current) : null;\n progressText = now ? [\n t('upload.amount', {loaded: formatFileSize(now.loaded), total: formatFileSize(now.total)}),\n now.bytesPerSecond ? t('upload.speed', {size: formatFileSize(now.bytesPerSecond)}) : null,\n now.remainingSeconds != null ? t('upload.remaining', {count: now.remainingSeconds}) : null,\n ].filter(Boolean).join(PROGRESS_SEPARATOR) : t('upload.starting');\n } else if (uploadSnapshot.matches('cancelled')) {\n progressText = t('upload.cancelled');\n }\n const buttonHint = limitsText ?? (picker.maxBytes ? t('upload.maxSizeHint', {max: formatFileSize(picker.maxBytes)}) : '');\n const downloadUploaded = () => startDownload({\n label: shownFile?.name ?? t('upload.download'),\n request: (transferOptions) => download(value, transferOptions),\n });\n\n const row = (\n <div>\n <FileUploadRoot\n direction=\"row\"\n aria-busy={isUploading}\n {...picker.dropHandlers}\n ownSx={{\n // en el teléfono el nombre baja a su renglón; en escritorio se corta con «…» y el botón no se mueve\n alignItems: 'center', justifyContent: 'flex-end', flexWrap: {xs: 'wrap', sm: 'nowrap'}, gap: 1, minWidth: 0, minHeight: 40,\n px: 0.5, mx: -0.5, borderRadius: 1,\n outline: picker.isDragging ? 2 : 0, outlineStyle: 'dashed', outlineColor: 'primary.main',\n bgcolor: picker.isDragging ? 'action.hover' : 'transparent',\n }}\n sx={sx}\n >\n <input {...picker.inputProps} id={inputId} name={name} aria-labelledby={label ? labelId : undefined} />\n {shownFile && (\n // el nombre cortado si es largo y completo al pasar el cursor; en el teléfono baja a su renglón\n <Tooltip title={shownFile.size != null ? `${shownFile.name} · ${formatFileSize(shownFile.size)}` : shownFile.name}>\n <FileUploadName variant=\"caption\" color=\"text.secondary\" noWrap\n ownSx={{minWidth: 0, flexShrink: 1, flexBasis: {xs: '100%', sm: 'auto'}, textAlign: {xs: 'right', sm: 'left'}}}>\n {shownFile.name}\n </FileUploadName>\n </Tooltip>\n )}\n {actions}\n <FileUploadStatus\n size=\"small\"\n color={STATUS_COLORS[status]}\n variant={status === FILE_UPLOAD_STATUS.pending ? 'filled' : 'outlined'}\n label={statusLabel}\n icon={(isUploading && (\n <CircularProgress size={12} color=\"inherit\" variant={isProcessing ? 'indeterminate' : 'determinate'} value={progress ?? 0} />\n )) || (isUploaded && <CheckCircleRoundedIcon />) || undefined}\n ownSx={{flexShrink: 0}}\n />\n {isUploading ? (\n <FileUploadButton size=\"small\" color=\"inherit\" onClick={cancel} ownSx={{flexShrink: 0}}>{t('upload.cancel')}</FileUploadButton>\n ) : (\n // describeChild: lo que acepta se lee como descripción; el nombre sigue siendo «Cargar» y el documento\n <Tooltip title={buttonHint} describeChild>\n <FileUploadButton\n size=\"small\"\n variant={isUploaded ? 'outlined' : 'contained'}\n startIcon={<FileUploadOutlinedIcon />}\n disabled={disabled}\n onClick={picker.open}\n aria-labelledby={label ? `${buttonTextId} ${labelId}` : undefined}\n ownSx={{flexShrink: 0}}\n >\n <span id={buttonTextId}>{isUploaded ? t('upload.replace') : t('upload.upload')}</span>\n </FileUploadButton>\n </Tooltip>\n )}\n {isUploaded && download && (\n <Tooltip title={t('upload.download')}>\n <IconButton size=\"small\" aria-label={t('upload.download')} onClick={downloadUploaded}>\n <FileDownloadOutlinedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n {isUploaded && !disabled && (\n <Tooltip title={t('upload.remove', {name: shownFile?.name ?? ''})}>\n <IconButton size=\"small\" aria-label={t('upload.remove', {name: shownFile?.name ?? ''})} onClick={remove}>\n <CloseRoundedIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </FileUploadRoot>\n {/* abajo: el error manda; si no, el avance; si no, la ayuda de la pantalla */}\n <FileUploadHelper error={Boolean(errorText)} role={errorText ? 'alert' : (progressText ? 'status' : undefined)}\n ownSx={{textAlign: 'right', mx: 0}}>\n {errorText ?? progressText ?? helperText}\n </FileUploadHelper>\n </div>\n );\n\n // sin etiqueta, el renglón va solo (la pantalla pone el título del documento a su manera)\n if (!label) {\n return row;\n }\n return <FieldFrame inputId={inputId} labelId={labelId} label={label} required={required}>{row}</FieldFrame>;\n}\n\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst FileUploadInput = withPieceSlots(FileUploadInputContent, THEME_NAME);\nexport default FileUploadInput;\n"],"names":["result","transfer"],"mappings":"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"}
@@ -56,6 +56,7 @@ function ImageInputContent(props) {
56
56
  chooseLabel,
57
57
  removeLabel,
58
58
  disabled = false,
59
+ required = false,
59
60
  busy = false,
60
61
  onReject,
61
62
  sx,
@@ -77,7 +78,7 @@ function ImageInputContent(props) {
77
78
  };
78
79
  const picker = useFilePicker({ accept, maxSize, disabled: isLocked, onAccept: change, onReject, texts });
79
80
  const pickText = chooseLabel ?? t(value ? "upload.changeImage" : "upload.chooseImage");
80
- return /* @__PURE__ */ jsx(FieldFrame, { inputId: previewId, label, children: /* @__PURE__ */ jsxs("div", { children: [
81
+ return /* @__PURE__ */ jsx(FieldFrame, { inputId: previewId, label, required, children: /* @__PURE__ */ jsxs("div", { children: [
81
82
  /* @__PURE__ */ jsxs(ImageRoot, { direction: "row", "aria-busy": busy, ownSx: { alignItems: "center", gap: 2 }, sx, children: [
82
83
  /* @__PURE__ */ jsx("input", { ...picker.inputProps, name, "aria-hidden": "true", tabIndex: -1 }),
83
84
  /* @__PURE__ */ jsx(
@@ -1 +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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} 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 {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction ImageInputContent(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, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const generatedId = useId();\n const previewId = `${name ?? 'image'}-${generatedId}`;\n const errorText = validationTextOf(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={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\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst ImageInput = withPieceSlots(ImageInputContent, THEME_NAME);\nexport default ImageInput;\n"],"names":[],"mappings":"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"}
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 {useValidationText} from '@/forms/useValidationText.js';\nimport FieldFrame from '@/inputs/FieldFrame.jsx';\nimport {piecePart, usePieceProps, withPieceSlots} 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.required] obligatorio: el asterisco en la etiqueta (lo revisa el esquema del formulario)\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 {Record<string, string|object>} [props.messages] el texto del error de este campo, por regla de validación\n * ({required: 'Escriba el RFC', matches: 'El RFC lleva 12 o 13 caracteres'}): gana al de Calcita y al de los textos\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nfunction ImageInputContent(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, required = false, busy = false, onReject, sx, messages, texts,\n ...unknownProps\n } = usePieceProps(props, THEME_NAME);\n warnUnknownProps(THEME_NAME, unknownProps);\n const {t} = useToolkitTranslation('common', texts);\n // el texto del error: el de la pantalla (messages), el del proyecto para este campo o el de Calcita\n const validationTextOf = useValidationText({name, label, messages});\n const generatedId = useId();\n const previewId = `${name ?? 'image'}-${generatedId}`;\n const errorText = validationTextOf(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} required={required}>\n <div>\n <ImageRoot direction=\"row\" aria-busy={busy} ownSx={{alignItems: 'center', gap: 2}} sx={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\n// lo que se exporta: la pieza con slots y slotProps para cambiar sus partes (themePieces.js)\nconst ImageInput = withPieceSlots(ImageInputContent, THEME_NAME);\nexport default ImageInput;\n"],"names":[],"mappings":"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"}