contentoh-components-library 21.6.49 → 21.6.50

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 (90) hide show
  1. package/.claude/launch.json +14 -0
  2. package/.env.test +1 -0
  3. package/dist/ai/prompts/attribute.prompt.js +28 -0
  4. package/dist/ai/prompts/description.prompt.js +33 -0
  5. package/dist/ai/schemas/attribute.schema.js +35 -0
  6. package/dist/ai/schemas/description.schema.js +27 -0
  7. package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
  8. package/dist/components/atoms/AssetPreview/index.js +150 -0
  9. package/dist/components/atoms/AssetPreview/styles.js +28 -0
  10. package/dist/components/atoms/IconFile/index.js +85 -2
  11. package/dist/components/atoms/IconFile/styles.js +1 -1
  12. package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
  13. package/dist/components/atoms/ProductImage/index.js +100 -27
  14. package/dist/components/atoms/ProductImage/styles.js +3 -1
  15. package/dist/components/molecules/EditionActiveImage/index.js +4 -2
  16. package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
  17. package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
  18. package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
  19. package/dist/components/molecules/GalleryElement/index.js +13 -4
  20. package/dist/components/molecules/GalleryHeader/index.js +96 -24
  21. package/dist/components/molecules/GalleryHeader/styles.js +4 -1
  22. package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
  23. package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
  24. package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
  25. package/dist/components/molecules/ImageSelector/index.js +1 -0
  26. package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
  27. package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
  28. package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
  29. package/dist/components/organisms/ImagePreviewer/index.js +1 -0
  30. package/dist/components/organisms/ProductImageModal/index.js +3 -2
  31. package/dist/components/organisms/PropagateChangesModal/index.js +12 -1
  32. package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
  33. package/dist/components/pages/ProviderProductEdition/index.js +11 -1
  34. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
  35. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
  36. package/dist/components/pages/RetailerProductEdition/index.js +605 -414
  37. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  38. package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
  39. package/dist/global-files/data.js +16 -3
  40. package/dist/global-files/digital-assets/assetTypes.js +480 -0
  41. package/dist/global-files/digital-assets/assets.actions.js +64 -0
  42. package/dist/global-files/digital-assets/assets.adapter.js +304 -0
  43. package/dist/global-files/digital-assets/assets.mock.js +136 -0
  44. package/dist/global-files/digital-assets/assets.reducer.js +220 -0
  45. package/dist/global-files/digital-assets/uploadAssets.js +179 -0
  46. package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
  47. package/dist/index.js +142 -90
  48. package/docs/activos-digitales.md +273 -0
  49. package/package.json +1 -1
  50. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
  51. package/src/components/atoms/AssetPreview/index.js +119 -0
  52. package/src/components/atoms/AssetPreview/styles.js +190 -0
  53. package/src/components/atoms/IconFile/index.js +28 -1
  54. package/src/components/atoms/IconFile/styles.js +8 -0
  55. package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
  56. package/src/components/atoms/ProductImage/index.js +95 -20
  57. package/src/components/atoms/ProductImage/styles.js +92 -7
  58. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  59. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
  60. package/src/components/molecules/GalleryAssetElement/index.js +162 -0
  61. package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
  62. package/src/components/molecules/GalleryElement/index.js +9 -0
  63. package/src/components/molecules/GalleryHeader/index.js +65 -13
  64. package/src/components/molecules/GalleryHeader/styles.js +2 -1
  65. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
  66. package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
  67. package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
  68. package/src/components/molecules/ImageSelector/index.js +1 -0
  69. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
  70. package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
  71. package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
  72. package/src/components/organisms/ImagePreviewer/index.js +1 -0
  73. package/src/components/organisms/ProductImageModal/index.js +1 -0
  74. package/src/components/organisms/PropagateChangesModal/index.js +11 -1
  75. package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
  76. package/src/components/pages/ProviderProductEdition/index.js +10 -1
  77. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
  78. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
  79. package/src/components/pages/RetailerProductEdition/index.js +366 -140
  80. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  81. package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
  82. package/src/global-files/data.js +17 -2
  83. package/src/global-files/digital-assets/assetTypes.js +419 -0
  84. package/src/global-files/digital-assets/assets.actions.js +50 -0
  85. package/src/global-files/digital-assets/assets.adapter.js +229 -0
  86. package/src/global-files/digital-assets/assets.mock.js +122 -0
  87. package/src/global-files/digital-assets/assets.reducer.js +197 -0
  88. package/src/global-files/digital-assets/uploadAssets.js +109 -0
  89. package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
  90. package/src/index.js +4 -0
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.warnAssetFile = exports.validateAssetFile = exports.validateAssetDuration = exports.suggestAssetType = exports.resolvePreviewKind = exports.humanizeDuration = exports.getExtension = exports.getAssetType = exports.formatDuration = exports.formatBytes = exports.buildDropzoneAccept = exports.assetTypeOptions = exports.acceptedMimes = exports.acceptedFormatsLabel = exports.acceptedExtensions = exports.VIDEO_DURATION_SECONDS = exports.PLAYABLE_VIDEO_EXTENSIONS = exports.MAX_ASSETS_PER_PRODUCT = exports.IMAGE_EXTENSIONS = exports.ASSET_TYPE_ORDER = exports.ASSET_TYPES = exports.ASSET_PREVIEW = void 0;
9
+
10
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/toConsumableArray"));
11
+
12
+ /**
13
+ * Catálogo de activos digitales.
14
+ *
15
+ * Este archivo es la ÚNICA fuente de verdad de qué se puede subir a la bandeja
16
+ * y con qué límites. Ajustar un tipo o un tope es cambiar un dato aquí, no
17
+ * tocar lógica repartida por la pantalla.
18
+ *
19
+ * Un activo NO es un "tipo de toma". Las imágenes de producto se validan contra
20
+ * `image_manager` (orden, obligatoriedad, cupo por cadena); los activos no
21
+ * entran a ese cruce y por eso tienen su propio catálogo.
22
+ *
23
+ * DOS COSAS QUE PARECEN DETALLE Y NO LO SON:
24
+ *
25
+ * 1. El tipo se decide por EXTENSIÓN, no por mime. El navegador no sabe qué es
26
+ * un .psd, un .ai o un .indd: reporta `application/octet-stream`, cadena
27
+ * vacía, o —peor— `application/pdf` para un .ai. Validar por mime dejaría
28
+ * fuera justo los formatos editables que se pidieron.
29
+ * 2. Por lo mismo, el `accept` del dropzone se arma con extensiones además de
30
+ * mimes. Si solo llevara mimes, react-dropzone descartaría esos archivos
31
+ * antes de que nuestra validación llegue a verlos, y el usuario no vería
32
+ * ningún mensaje: simplemente no pasaría nada al soltarlos.
33
+ */
34
+ var KB = 1024;
35
+ var MB = 1024 * KB;
36
+ /**
37
+ * Cómo se pinta un archivo en pantalla. No es lo mismo que el tipo de activo:
38
+ * un 360 puede ser mp4 (se reproduce) o gif (se muestra), así que la forma de
39
+ * previsualizar se resuelve por el archivo real, no por su categoría.
40
+ */
41
+
42
+ var ASSET_PREVIEW = {
43
+ VIDEO: "video",
44
+ IMAGE: "image",
45
+ ICON: "icon"
46
+ };
47
+ /**
48
+ * PROVISIONAL: cantidad máxima de activos por producto. Es el único dato que
49
+ * el manager no respondió. Subido de 10 a 20 porque la lista de tipos creció
50
+ * (documentos y editables, no solo multimedia).
51
+ */
52
+
53
+ exports.ASSET_PREVIEW = ASSET_PREVIEW;
54
+ var MAX_ASSETS_PER_PRODUCT = 20;
55
+ /**
56
+ * Duración válida de un video.
57
+ *
58
+ * Hoy la ficha valida `duracion <= 10 || duracion >= 600` mientras el mensaje al
59
+ * usuario dice "entre 15 segundos y 10 minutos". Los dos no pueden ser ciertos.
60
+ * Se conserva el rango que el código valida de verdad, y el mensaje se genera
61
+ * desde estos mismos números para que no se vuelvan a desincronizar.
62
+ */
63
+
64
+ exports.MAX_ASSETS_PER_PRODUCT = MAX_ASSETS_PER_PRODUCT;
65
+ var VIDEO_DURATION_SECONDS = {
66
+ min: 10,
67
+ max: 600
68
+ };
69
+ exports.VIDEO_DURATION_SECONDS = VIDEO_DURATION_SECONDS;
70
+ var ASSET_TYPES = {
71
+ 360: {
72
+ id: "360",
73
+ label: "360",
74
+ description: "Giro del producto, en mp4 o gif animado",
75
+ mimes: ["video/mp4", "image/gif"],
76
+ extensions: ["mp4", "gif"],
77
+ maxBytes: 200 * MB,
78
+ validatesDuration: true
79
+ },
80
+ video: {
81
+ id: "video",
82
+ label: "Video",
83
+ description: "Video de producto o promocional",
84
+ mimes: ["video/mp4", "video/quicktime", "video/webm"],
85
+ extensions: ["mp4", "mov", "webm"],
86
+ maxBytes: 200 * MB,
87
+ validatesDuration: true
88
+ },
89
+ gif: {
90
+ id: "gif",
91
+ label: "GIF",
92
+ description: "Animación corta",
93
+ mimes: ["image/gif"],
94
+ extensions: ["gif"],
95
+ maxBytes: 20 * MB,
96
+ validatesDuration: false
97
+ },
98
+ documento: {
99
+ id: "documento",
100
+ label: "Documento",
101
+ description: "Manual, ficha técnica, certificado",
102
+ mimes: ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
103
+ extensions: ["pdf", "doc", "docx"],
104
+ maxBytes: 50 * MB,
105
+ validatesDuration: false
106
+ },
107
+ hoja_calculo: {
108
+ id: "hoja_calculo",
109
+ label: "Hoja de cálculo",
110
+ description: "Listas de medidas, tablas de tallas, matrices",
111
+ mimes: ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv"],
112
+ extensions: ["xlsx", "xls", "csv"],
113
+ maxBytes: 20 * MB,
114
+ validatesDuration: false
115
+ },
116
+
117
+ /*
118
+ Archivos de diseño editables. Es la categoría con más riesgo: un .psd con
119
+ capas o un .aep pasan sin esfuerzo de 1 GB, y ninguno se puede previsualizar
120
+ en el navegador —se muestran con icono y nombre—. El tope de aquí abajo es
121
+ una propuesta, no un dato confirmado.
122
+ */
123
+ editable: {
124
+ id: "editable",
125
+ label: "Editable",
126
+ description: "Archivo de diseño con capas (Photoshop, Illustrator, InDesign…)",
127
+ mimes: [],
128
+ extensions: ["psd", "psb", // Photoshop
129
+ "ai", "eps", // Illustrator
130
+ "indd", "idml", // InDesign
131
+ "aep", // After Effects
132
+ "prproj", // Premiere
133
+ "sketch", "fig", "xd", // Sketch, Figma, Adobe XD
134
+ "afphoto", "afdesign", // Affinity
135
+ "svg"],
136
+ maxBytes: 500 * MB,
137
+ validatesDuration: false
138
+ },
139
+ comprimido: {
140
+ id: "comprimido",
141
+ label: "Comprimido",
142
+ description: "Paquete de varios archivos en un solo zip",
143
+ mimes: ["application/zip", "application/x-rar-compressed", "application/x-7z-compressed"],
144
+ extensions: ["zip", "rar", "7z"],
145
+ maxBytes: 500 * MB,
146
+ validatesDuration: false
147
+ },
148
+ otro: {
149
+ id: "otro",
150
+ label: "Otro",
151
+ description: "Formato no reconocido",
152
+ mimes: [],
153
+ extensions: [],
154
+ maxBytes: 50 * MB,
155
+ validatesDuration: false
156
+ }
157
+ };
158
+ /** Orden en que se muestran en el selector de tipo. */
159
+
160
+ exports.ASSET_TYPES = ASSET_TYPES;
161
+ var ASSET_TYPE_ORDER = ["360", "video", "gif", "documento", "hoja_calculo", "editable", "comprimido", "otro"];
162
+ /** Opciones listas para el componente `Select` (espera `{ value, name }`). */
163
+
164
+ exports.ASSET_TYPE_ORDER = ASSET_TYPE_ORDER;
165
+
166
+ var assetTypeOptions = function assetTypeOptions() {
167
+ return ASSET_TYPE_ORDER.map(function (id) {
168
+ return {
169
+ value: id,
170
+ name: ASSET_TYPES[id].label
171
+ };
172
+ });
173
+ };
174
+ /** Extensiones que el navegador pinta como imagen. */
175
+
176
+
177
+ exports.assetTypeOptions = assetTypeOptions;
178
+ var IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "bmp", "avif", "svg", "tiff"];
179
+ /**
180
+ * Formatos de video que un navegador REPRODUCE de verdad.
181
+ *
182
+ * No es lo mismo que "es un video". Un .mov se sube bien y S3 lo sirve como
183
+ * video/quicktime, pero Chrome y Firefox no lo reproducen: una grabación de
184
+ * pantalla de macOS viene en HEVC o ProRes, que no saben decodificar. Tratarlo
185
+ * como reproducible pinta un recuadro negro sin explicación; mostrarlo como
186
+ * archivo dice la verdad y deja claro lo que sí se puede hacer: descargarlo.
187
+ */
188
+
189
+ exports.IMAGE_EXTENSIONS = IMAGE_EXTENSIONS;
190
+ var PLAYABLE_VIDEO_EXTENSIONS = ["mp4", "webm", "ogv", "ogg"];
191
+ /**
192
+ * Cómo pintar un archivo: manda la extensión y el mime queda de respaldo.
193
+ *
194
+ * La extensión manda porque el mime miente en las dos direcciones: un .psd
195
+ * llega sin él, y un .mov llega con uno que empieza por `video/` aunque el
196
+ * navegador no sepa reproducirlo.
197
+ *
198
+ * NUNCA por la URL. `ProductImage` lo hacía con `/\.mp4$/` y por eso fallaba:
199
+ * la URL guardada apunta al redimensionador de CloudFront y no conserva la
200
+ * extensión del archivo, así que un video jamás daba positivo.
201
+ */
202
+
203
+ exports.PLAYABLE_VIDEO_EXTENSIONS = PLAYABLE_VIDEO_EXTENSIONS;
204
+
205
+ var resolvePreviewKind = function resolvePreviewKind() {
206
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
207
+ mime = _ref.mime,
208
+ ext = _ref.ext,
209
+ name = _ref.name;
210
+
211
+ var extension = (ext || getExtension(name) || "").toLowerCase();
212
+
213
+ if (extension) {
214
+ if (PLAYABLE_VIDEO_EXTENSIONS.includes(extension)) return ASSET_PREVIEW.VIDEO;
215
+ if (IMAGE_EXTENSIONS.includes(extension)) return ASSET_PREVIEW.IMAGE;
216
+ return ASSET_PREVIEW.ICON;
217
+ }
218
+
219
+ if (mime === "video/mp4" || mime === "video/webm") return ASSET_PREVIEW.VIDEO;
220
+ if (mime !== null && mime !== void 0 && mime.startsWith("image/")) return ASSET_PREVIEW.IMAGE;
221
+ return ASSET_PREVIEW.ICON;
222
+ };
223
+
224
+ exports.resolvePreviewKind = resolvePreviewKind;
225
+
226
+ var getAssetType = function getAssetType(assetTypeId) {
227
+ var _ASSET_TYPES$assetTyp;
228
+
229
+ return (_ASSET_TYPES$assetTyp = ASSET_TYPES[assetTypeId]) !== null && _ASSET_TYPES$assetTyp !== void 0 ? _ASSET_TYPES$assetTyp : ASSET_TYPES.otro;
230
+ };
231
+
232
+ exports.getAssetType = getAssetType;
233
+
234
+ var getExtension = function getExtension() {
235
+ var fileName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
236
+ var parts = String(fileName).split(".");
237
+ return parts.length > 1 ? parts.pop().toLowerCase() : "";
238
+ };
239
+ /*
240
+ Qué tipo reclama cada extensión, resuelto una sola vez.
241
+
242
+ El orden de `ASSET_TYPE_ORDER` decide los empates: `mp4` y `gif` aparecen
243
+ también en "360", pero gana el tipo genérico porque es el caso común y el
244
+ usuario puede reetiquetarlo en la tarjeta. Mirando el archivo no hay forma de
245
+ saber si un giro es un giro.
246
+ */
247
+
248
+
249
+ exports.getExtension = getExtension;
250
+ var EXTENSION_INDEX = ASSET_TYPE_ORDER.reduce(function (index, id) {
251
+ ASSET_TYPES[id].extensions.forEach(function (ext) {
252
+ if (id !== "360" && !index[ext]) index[ext] = id;
253
+ });
254
+ return index;
255
+ }, {});
256
+ /**
257
+ * Tipo de activo sugerido para un archivo recién elegido.
258
+ *
259
+ * Manda la extensión. El mime solo se consulta como respaldo para los formatos
260
+ * multimedia, que son los únicos que el navegador reconoce con certeza: para un
261
+ * .psd o un .ai reporta `application/octet-stream` o nada, y para un .ai a veces
262
+ * `application/pdf`, que lo clasificaría como documento.
263
+ */
264
+
265
+ var suggestAssetType = function suggestAssetType() {
266
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
267
+ mime = _ref2.mime,
268
+ name = _ref2.name;
269
+
270
+ var ext = getExtension(name);
271
+ if (EXTENSION_INDEX[ext]) return EXTENSION_INDEX[ext];
272
+ if (mime !== null && mime !== void 0 && mime.startsWith("video/")) return "video";
273
+ if (mime === "image/gif") return "gif";
274
+ if (mime === "application/pdf") return "documento";
275
+ return "otro";
276
+ };
277
+ /** Todos los mimes aceptados, sin repetir. */
278
+
279
+
280
+ exports.suggestAssetType = suggestAssetType;
281
+
282
+ var acceptedMimes = function acceptedMimes() {
283
+ return (0, _toConsumableArray2.default)(new Set(ASSET_TYPE_ORDER.flatMap(function (id) {
284
+ return ASSET_TYPES[id].mimes;
285
+ })));
286
+ };
287
+ /** Todas las extensiones aceptadas, sin repetir y con punto. */
288
+
289
+
290
+ exports.acceptedMimes = acceptedMimes;
291
+
292
+ var acceptedExtensions = function acceptedExtensions() {
293
+ return (0, _toConsumableArray2.default)(new Set(ASSET_TYPE_ORDER.flatMap(function (id) {
294
+ return ASSET_TYPES[id].extensions;
295
+ }))).map(function (ext) {
296
+ return ".".concat(ext);
297
+ });
298
+ };
299
+ /**
300
+ * Valor de `accept` para react-dropzone (v12: una cadena separada por comas).
301
+ *
302
+ * Lleva mimes Y extensiones. Con solo mimes, react-dropzone descartaría un
303
+ * .psd o un .indd antes de que nuestra validación llegue a verlos —el navegador
304
+ * no les asigna mime— y al soltarlos no pasaría absolutamente nada: ni el
305
+ * archivo, ni un mensaje de por qué.
306
+ */
307
+
308
+
309
+ exports.acceptedExtensions = acceptedExtensions;
310
+
311
+ var buildDropzoneAccept = function buildDropzoneAccept() {
312
+ return [].concat((0, _toConsumableArray2.default)(acceptedMimes()), (0, _toConsumableArray2.default)(acceptedExtensions())).join(", ");
313
+ };
314
+ /** Lista legible de formatos, para mensajes y para la ayuda de la bandeja. */
315
+
316
+
317
+ exports.buildDropzoneAccept = buildDropzoneAccept;
318
+
319
+ var acceptedFormatsLabel = function acceptedFormatsLabel() {
320
+ return acceptedExtensions().map(function (ext) {
321
+ return ext.replace(".", "").toUpperCase();
322
+ }).join(", ");
323
+ };
324
+
325
+ exports.acceptedFormatsLabel = acceptedFormatsLabel;
326
+ var GB = 1024 * MB;
327
+
328
+ var formatBytes = function formatBytes(bytes) {
329
+ var value = Number(bytes);
330
+ if (!Number.isFinite(value) || value <= 0) return "-";
331
+ if (value < KB) return "".concat(value, " B");
332
+ if (value < MB) return "".concat(Math.round(value / KB), " KB"); // Los editables llegan a varios cientos de MB; sin este escalón un archivo de
333
+ // 1.2 GB se anunciaba como "1228.8 MB".
334
+
335
+ if (value < GB) return "".concat((value / MB).toFixed(1), " MB");
336
+ return "".concat((value / GB).toFixed(2), " GB");
337
+ };
338
+ /** Duración compacta para la tarjeta del activo: "1:05". */
339
+
340
+
341
+ exports.formatBytes = formatBytes;
342
+
343
+ var formatDuration = function formatDuration(seconds) {
344
+ var value = Number(seconds);
345
+ if (!Number.isFinite(value) || value <= 0) return "-";
346
+ var mins = Math.floor(value / 60);
347
+ var secs = Math.round(value % 60);
348
+ return "".concat(mins, ":").concat(String(secs).padStart(2, "0"));
349
+ };
350
+ /** Duración en prosa para los mensajes de error: "10 segundos", "10 minutos". */
351
+
352
+
353
+ exports.formatDuration = formatDuration;
354
+
355
+ var humanizeDuration = function humanizeDuration(seconds) {
356
+ var value = Number(seconds);
357
+ if (!Number.isFinite(value) || value <= 0) return "-";
358
+ if (value < 60) return "".concat(Math.round(value), " segundos");
359
+ var mins = value / 60;
360
+ var label = Number.isInteger(mins) ? mins : mins.toFixed(1);
361
+ return "".concat(label, " ").concat(mins === 1 ? "minuto" : "minutos");
362
+ };
363
+ /**
364
+ * Valida un archivo ANTES de tocar la red: tipo permitido, peso y cupo.
365
+ *
366
+ * Se separa de la subida a propósito. Da igual que el archivo termine yendo por
367
+ * URL prefirmada o por el `putObject` de hoy: esto no cambia, y es lo que evita
368
+ * que el navegador empiece a leer 200 MB que de todos modos se van a rechazar.
369
+ *
370
+ * @returns {{ valid: boolean, code?: string, message?: string }}
371
+ */
372
+
373
+
374
+ exports.humanizeDuration = humanizeDuration;
375
+
376
+ var validateAssetFile = function validateAssetFile(file) {
377
+ var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
378
+ _ref3$currentCount = _ref3.currentCount,
379
+ currentCount = _ref3$currentCount === void 0 ? 0 : _ref3$currentCount;
380
+
381
+ if (!file) {
382
+ return {
383
+ valid: false,
384
+ code: "NO_FILE",
385
+ message: "No se recibió ningún archivo."
386
+ };
387
+ }
388
+
389
+ if (currentCount >= MAX_ASSETS_PER_PRODUCT) {
390
+ return {
391
+ valid: false,
392
+ code: "MAX_ASSETS",
393
+ message: "Solo puedes almacenar ".concat(MAX_ASSETS_PER_PRODUCT, " activos digitales por producto.")
394
+ };
395
+ }
396
+
397
+ var assetTypeId = suggestAssetType({
398
+ mime: file.type,
399
+ name: file.name
400
+ });
401
+ var assetType = getAssetType(assetTypeId);
402
+
403
+ if (assetTypeId === "otro") {
404
+ return {
405
+ valid: false,
406
+ code: "UNSUPPORTED_TYPE",
407
+ message: "\"".concat(file.name, "\" no es un formato permitido. Se aceptan: ").concat(acceptedFormatsLabel(), ".")
408
+ };
409
+ }
410
+
411
+ if (file.size > assetType.maxBytes) {
412
+ return {
413
+ valid: false,
414
+ code: "MAX_SIZE",
415
+ message: "\"".concat(file.name, "\" pesa ").concat(formatBytes(file.size), " y el m\xE1ximo para ").concat(assetType.label, " es ").concat(formatBytes(assetType.maxBytes), ".")
416
+ };
417
+ }
418
+
419
+ return {
420
+ valid: true,
421
+ assetType: assetTypeId,
422
+ warning: warnAssetFile(file, assetTypeId)
423
+ };
424
+ };
425
+ /**
426
+ * Aviso para un archivo que SÍ se acepta pero que el usuario no va a poder ver.
427
+ *
428
+ * El caso que sorprende es el video: alguien sube una grabación de pantalla de
429
+ * macOS —un .mov— esperando reproducirla, y el navegador no la decodifica. El
430
+ * archivo sirve igual para entregarlo al retailer, así que no se rechaza; pero
431
+ * enterarse al soltarlo es mucho mejor que enterarse al intentar verlo.
432
+ *
433
+ * No se avisa de un .psd o un .indd: nadie espera previsualizar un archivo de
434
+ * diseño, y un aviso por cada uno sería ruido.
435
+ */
436
+
437
+
438
+ exports.validateAssetFile = validateAssetFile;
439
+
440
+ var warnAssetFile = function warnAssetFile(file, assetTypeId) {
441
+ if (assetTypeId !== "video" && assetTypeId !== "360") return null;
442
+ var ext = getExtension(file === null || file === void 0 ? void 0 : file.name);
443
+ if (PLAYABLE_VIDEO_EXTENSIONS.includes(ext)) return null;
444
+ return "\"".concat(file === null || file === void 0 ? void 0 : file.name, "\" se guardar\xE1 y se podr\xE1 descargar, pero el navegador no puede reproducir los .").concat(ext, ". Convi\xE9rtelo a MP4 si necesitas verlo aqu\xED.");
445
+ };
446
+ /**
447
+ * Valida la duración de un video. Va aparte de `validateAssetFile` porque
448
+ * obtenerla es asíncrono (hay que cargar el archivo en un <video>) y no se
449
+ * quiere pagar ese costo en archivos que ya fueron rechazados por peso.
450
+ */
451
+
452
+
453
+ exports.warnAssetFile = warnAssetFile;
454
+
455
+ var validateAssetDuration = function validateAssetDuration(seconds, assetTypeId) {
456
+ var assetType = getAssetType(assetTypeId);
457
+ if (!assetType.validatesDuration) return {
458
+ valid: true
459
+ };
460
+ var value = Number(seconds);
461
+ if (!Number.isFinite(value)) return {
462
+ valid: true
463
+ };
464
+ var min = VIDEO_DURATION_SECONDS.min,
465
+ max = VIDEO_DURATION_SECONDS.max;
466
+
467
+ if (value < min || value > max) {
468
+ return {
469
+ valid: false,
470
+ code: "DURATION",
471
+ message: "Los videos deben durar entre ".concat(humanizeDuration(min), " y ").concat(humanizeDuration(max), ".")
472
+ };
473
+ }
474
+
475
+ return {
476
+ valid: true
477
+ };
478
+ };
479
+
480
+ exports.validateAssetDuration = validateAssetDuration;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.assetsInitialState = exports.ASSET_ACTIONS = void 0;
7
+
8
+ /**
9
+ * Vocabulario compartido del estado de activos digitales.
10
+ *
11
+ * Vive aquí, y no dentro del reducer, para que nadie dependa de nadie en la
12
+ * dirección equivocada: el reducer de la ficha (`context/reducers/assets.js`)
13
+ * y el hook de la bandeja (`useDigitalAssets`) importan ambos estas constantes,
14
+ * y ninguno de los dos importa al otro. Es lo que permite probar el reducer
15
+ * suelto en Storybook sin arrastrar la pantalla completa.
16
+ */
17
+ var ASSET_ACTIONS = {
18
+ /** Reemplaza toda la lista con lo que devolvió el backend. Limpia el rastreo. */
19
+ SET_ASSETS_VALUES: "SET_ASSETS_VALUES",
20
+
21
+ /** Agrega activos recién soltados y ya validados. */
22
+ ADD_ASSETS: "ADD_ASSETS",
23
+
24
+ /**
25
+ * Cambio hecho por el usuario (reetiquetar el tipo, renombrar). Si el activo
26
+ * ya existe en el backend, queda marcado para enviarse al guardar.
27
+ */
28
+ UPDATE_ASSET: "UPDATE_ASSET",
29
+
30
+ /**
31
+ * Cambio de ciclo de vida de la subida: progreso, estado, error, o la clave
32
+ * de S3 que devuelve el backend. NO marca nada para guardar —el progreso de
33
+ * una barra no es una edición del usuario— y por eso está separado de
34
+ * `UPDATE_ASSET` en vez de ser una bandera.
35
+ */
36
+ PATCH_ASSET: "PATCH_ASSET",
37
+
38
+ /** Quita un activo de la lista. Si ya existía en el backend, lo anota para borrarlo. */
39
+ REMOVE_ASSET: "REMOVE_ASSET",
40
+ SET_SELECTED_ASSETS: "SET_SELECTED_ASSETS",
41
+ TOGGLE_CHECK_ALL_ASSETS: "TOGGLE_CHECK_ALL_ASSETS",
42
+
43
+ /** Tras un guardado exitoso: olvida qué cambió y qué se borró. */
44
+ CLEAR_ASSETS_TRACKING: "CLEAR_ASSETS_TRACKING"
45
+ };
46
+ exports.ASSET_ACTIONS = ASSET_ACTIONS;
47
+ var assetsInitialState = {
48
+ /** Lista completa, en el modelo de interfaz de `assets.adapter`. */
49
+ assets_values: [],
50
+
51
+ /**
52
+ * Claves de los activos YA GUARDADOS que el usuario modificó. Se guardan
53
+ * claves y no copias del objeto: una copia se queda vieja en cuanto el
54
+ * usuario vuelve a tocar el activo, y al guardar se enviaría el valor
55
+ * anterior. Con la clave, el objeto se lee siempre de `assets_values`.
56
+ */
57
+ updated_assets: [],
58
+
59
+ /** Ids del backend pendientes de borrar. */
60
+ deleted_assets: [],
61
+ selected_assets: [],
62
+ all_assets_checked: false
63
+ };
64
+ exports.assetsInitialState = assetsInitialState;