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,159 @@
1
+ import styled from "styled-components";
2
+ import { FontFamily, GlobalColors } from "../../../global-files/variables";
3
+
4
+ /*
5
+ Calcado de `GalleryElement/styles.js` para que imágenes y activos se lean como
6
+ una sola tabla. Cambia solo la rejilla de la fila —cuatro columnas en vez de
7
+ cinco, porque un activo no tiene leyenda— y lo propio de un archivo que se
8
+ está subiendo: barra de progreso, mensaje de error y botón de quitar.
9
+ */
10
+ export const Container = styled.div`
11
+ position: relative;
12
+
13
+ &.grid-gallery {
14
+ width: 179px;
15
+ border-radius: 3px;
16
+ overflow: hidden;
17
+
18
+ .buttons-container {
19
+ position: absolute;
20
+ top: 5px;
21
+ left: 5px;
22
+ z-index: 1;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ }
27
+
28
+ .asset-container {
29
+ border: 1px solid #f0eef2;
30
+
31
+ .information-container {
32
+ background-color: ${GlobalColors.s2};
33
+ display: flex;
34
+ justify-content: space-between;
35
+ padding: 0 2px;
36
+
37
+ p {
38
+ font-family: ${FontFamily.Lato};
39
+ color: ${GlobalColors.s4};
40
+ font-size: 11px;
41
+ line-height: 17px;
42
+ overflow: hidden;
43
+ text-overflow: ellipsis;
44
+ white-space: nowrap;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ &.row-gallery {
51
+ width: 100%;
52
+ display: grid;
53
+ grid-template-columns: 0.05fr 1fr;
54
+ align-items: center;
55
+ padding-right: 10px;
56
+
57
+ .buttons-container {
58
+ position: relative;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ }
63
+
64
+ .asset-container {
65
+ display: grid;
66
+ grid-template-columns: auto 1fr;
67
+ align-items: center;
68
+ flex: 1;
69
+
70
+ .information-container {
71
+ display: grid;
72
+ grid-template-columns: repeat(4, 1fr);
73
+ align-items: center;
74
+
75
+ p {
76
+ font-family: ${FontFamily.Lato};
77
+ color: ${GlobalColors.s4};
78
+ font-size: 14px;
79
+ line-height: 17px;
80
+ text-align: center;
81
+ overflow: hidden;
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
85
+
86
+ & > * {
87
+ flex: 1;
88
+ padding: 5px;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ /* El nombre es el sitio donde uno intenta hacer clic para abrir un archivo. */
95
+ .asset-link {
96
+ cursor: pointer;
97
+ text-decoration: underline;
98
+ text-underline-offset: 2px;
99
+
100
+ &:hover {
101
+ color: ${GlobalColors.color_purple};
102
+ }
103
+ }
104
+
105
+ &.asset-error {
106
+ border: 1px solid ${GlobalColors.secondary_magenta};
107
+ border-radius: 3px;
108
+ }
109
+
110
+ /* Progreso de subida. Ocupa el borde inferior de la fila para no empujar el
111
+ resto del contenido mientras aparece y desaparece. */
112
+ .asset-progress {
113
+ position: absolute;
114
+ left: 0;
115
+ right: 0;
116
+ bottom: 0;
117
+ height: 3px;
118
+ background: ${GlobalColors.s3};
119
+ border-radius: 2px;
120
+ overflow: hidden;
121
+ }
122
+
123
+ .asset-progress-fill {
124
+ height: 100%;
125
+ background: ${GlobalColors.color_purple};
126
+ transition: width 0.2s ease-out;
127
+ }
128
+
129
+ .asset-error-message {
130
+ margin: 2px 0 0;
131
+ padding: 0 8px;
132
+ font-family: ${FontFamily.Lato};
133
+ font-size: 11px;
134
+ line-height: 15px;
135
+ color: ${GlobalColors.secondary_magenta};
136
+ }
137
+
138
+ .asset-remove {
139
+ position: absolute;
140
+ top: 4px;
141
+ right: 6px;
142
+ width: 18px;
143
+ height: 18px;
144
+ display: grid;
145
+ place-items: center;
146
+ padding: 0;
147
+ border: none;
148
+ border-radius: 50%;
149
+ background: ${GlobalColors.s2};
150
+ color: ${GlobalColors.s5};
151
+ font-size: 14px;
152
+ line-height: 1;
153
+ cursor: pointer;
154
+
155
+ &:hover {
156
+ background: ${GlobalColors.s3};
157
+ }
158
+ }
159
+ `;
@@ -25,6 +25,7 @@ export const GalleryElement = ({
25
25
  auditImages,
26
26
  compare,
27
27
  updateApprovedInputsImages,
28
+ onSelect,
28
29
  }) => {
29
30
  const [showValidationPanel, setShowValidationPanel] = useState(false);
30
31
  const [showDivStates, setShowDivStates] = useState({});
@@ -93,7 +94,15 @@ export const GalleryElement = ({
93
94
  <ProductImage
94
95
  img={compare ? auditImages?.values[number]?.src : image?.src}
95
96
  altText={image?.altText}
97
+ // Sin `ext`, un video se pinta como <img> rota: la URL guardada no
98
+ // conserva la extensión del archivo.
99
+ ext={compare ? auditImages?.values[number]?.ext : image?.ext}
96
100
  imageType={`catalogue-image-${gridLayout ? "big" : "small"}-size`}
101
+ // La miniatura de un video ya no lleva controles, así que el clic es
102
+ // la única forma de llegar al visor grande. Se aplica también a las
103
+ // imágenes: dos recuadros idénticos donde solo uno responde al clic
104
+ // sería peor que no responder ninguno.
105
+ onClick={onSelect}
97
106
  />
98
107
  <div className="information-container">
99
108
  <p>{image?.name}</p>
@@ -1,28 +1,80 @@
1
1
  import { Container } from "./styles";
2
2
  import { CheckBox } from "../../atoms/CheckBox";
3
3
 
4
- export const GalleryHeader = ({ setCheckAll, setSelectedImages, shotThd }) => {
4
+ /*
5
+ Las columnas de la bandeja dependen de qué se está listando: un PDF no tiene
6
+ "tipo de toma" ni alto por ancho, y una foto no tiene "tipo de activo".
7
+ Estaban escritas a mano en el marcado, así que la única forma de cambiarlas
8
+ era duplicar el componente.
9
+
10
+ El preset `image` reproduce exactamente las columnas y clases de antes, para
11
+ que la bandeja de imágenes se vea idéntica sin que nadie tenga que pasar nada.
12
+ */
13
+ export const GALLERY_COLUMNS = {
14
+ image: {
15
+ template: "0.1fr 1fr 1fr 1fr 1fr 1fr",
16
+ columns: [
17
+ { key: "preview", label: "Imagen", className: "image-header" },
18
+ { key: "name", label: "Nombre", className: "image-name" },
19
+ { key: "size", label: "Tamaño", className: "image-size" },
20
+ { key: "format", label: "Formato", className: "image-format" },
21
+ {
22
+ key: "legend",
23
+ label: "¿Tiene leyenda?",
24
+ className: "image-description",
25
+ },
26
+ { key: "shotType", label: "Tipo de toma", className: "shoot-type" },
27
+ ],
28
+ },
29
+ asset: {
30
+ template: "0.1fr 1fr 1fr 1fr 1fr",
31
+ columns: [
32
+ { key: "preview", label: "Activo", className: "asset-header" },
33
+ { key: "name", label: "Nombre", className: "asset-name" },
34
+ { key: "weight", label: "Peso", className: "asset-weight" },
35
+ { key: "format", label: "Formato", className: "asset-format" },
36
+ { key: "assetType", label: "Tipo de activo", className: "asset-type" },
37
+ ],
38
+ },
39
+ };
40
+
41
+ /**
42
+ * Encabezado de la bandeja, con su casilla de "seleccionar todo".
43
+ *
44
+ * `checkAllId` e `itemSelector` se pueden cambiar porque imágenes y activos
45
+ * conviven en la misma pestaña: si las dos listas compartieran el id de la
46
+ * casilla global, marcar todas las imágenes marcaría también los activos.
47
+ */
48
+ export const GalleryHeader = ({
49
+ setCheckAll,
50
+ setSelectedImages,
51
+ shotThd,
52
+ variant = "image",
53
+ columns,
54
+ checkAllId = "check-all-images",
55
+ itemSelector = "[id^='gallery-element-']",
56
+ }) => {
57
+ const preset = GALLERY_COLUMNS[variant] ?? GALLERY_COLUMNS.image;
58
+ const visibleColumns = columns ?? preset.columns;
59
+
5
60
  const checkAll = (e) => {
6
- const checkboxList = document.querySelectorAll("[id^='gallery-element-']");
61
+ const checkboxList = document.querySelectorAll(itemSelector);
7
62
  checkboxList.forEach((chk) => (chk.checked = e.target.checked));
8
63
  setCheckAll(e.target.checked);
9
64
  if (!e.target.checked) setSelectedImages([]);
10
65
  };
66
+
11
67
  return (
12
- <Container>
68
+ <Container template={preset.template}>
13
69
  <div className="checkbox">
14
- <CheckBox id="check-all-images" onChange={(e) => checkAll(e)} />
70
+ <CheckBox id={checkAllId} onChange={(e) => checkAll(e)} />
15
71
  </div>
16
72
  <div className="information">
17
- <p className="image-header">Imagen</p>
18
- <p className="image-name">Nombre</p>
19
- <p className="image-size">Tamaño</p>
20
- <p className="image-format">Formato</p>
21
- <p className="image-description">¿Tiene leyenda?</p>
22
- <p className="shoot-type">Tipo de toma</p>
23
- {/* <p className="">Tipo de imagen</p> */}
24
- {/* <p className="">Tipo de empaque</p> */}
25
- {/* <p className="">Estatus</p> */}
73
+ {visibleColumns.map((column) => (
74
+ <p key={column.key} className={column.className}>
75
+ {column.label}
76
+ </p>
77
+ ))}
26
78
  </div>
27
79
  </Container>
28
80
  );
@@ -15,7 +15,8 @@ export const Container = styled.div`
15
15
  .information{
16
16
  width: 90%;
17
17
  display: grid;
18
- grid-template-columns: 0.1fr 1fr 1fr 1fr 1fr 1fr;
18
+ /* Lo define el preset de columnas; el valor por defecto es el de imágenes. */
19
+ grid-template-columns: ${({ template }) => template || "0.1fr 1fr 1fr 1fr 1fr 1fr"};
19
20
  }
20
21
  .image-header{
21
22
 
@@ -0,0 +1,32 @@
1
+ import { useState } from "react";
2
+ import { GalleryTrayTabs, TRAYS } from "./index";
3
+
4
+ export default {
5
+ title: "Components/molecules/GalleryTrayTabs",
6
+ component: GalleryTrayTabs,
7
+ };
8
+
9
+ export const Interactivo = () => {
10
+ const [tray, setTray] = useState(TRAYS.IMAGES);
11
+ return (
12
+ <GalleryTrayTabs
13
+ activeTray={tray}
14
+ setActiveTray={setTray}
15
+ imagesCount={8}
16
+ assetsCount={3}
17
+ />
18
+ );
19
+ };
20
+
21
+ /** Estado real hasta que exista el backend: la bandeja de activos en cero. */
22
+ export const SinActivos = () => {
23
+ const [tray, setTray] = useState(TRAYS.IMAGES);
24
+ return (
25
+ <GalleryTrayTabs
26
+ activeTray={tray}
27
+ setActiveTray={setTray}
28
+ imagesCount={8}
29
+ assetsCount={0}
30
+ />
31
+ );
32
+ };
@@ -0,0 +1,41 @@
1
+ import { Container } from "./styles";
2
+
3
+ export const TRAYS = {
4
+ IMAGES: "images",
5
+ ASSETS: "assets",
6
+ };
7
+
8
+ /**
9
+ * Selector entre las dos bandejas de la pestaña Imágenes.
10
+ *
11
+ * Los activos viven aquí y no en una pestaña propia porque es lo que pide la
12
+ * tarea —"dentro de las bandejas de imágenes"— y porque así comparten
13
+ * selección, descarga y permisos con las fotos en vez de duplicarlos.
14
+ */
15
+ export const GalleryTrayTabs = ({
16
+ activeTray = TRAYS.IMAGES,
17
+ setActiveTray,
18
+ imagesCount = 0,
19
+ assetsCount = 0,
20
+ }) => {
21
+ const trays = [
22
+ { id: TRAYS.IMAGES, label: "Imágenes", count: imagesCount },
23
+ { id: TRAYS.ASSETS, label: "Activos digitales", count: assetsCount },
24
+ ];
25
+
26
+ return (
27
+ <Container>
28
+ {trays.map((tray) => (
29
+ <button
30
+ key={tray.id}
31
+ type="button"
32
+ className={tray.id === activeTray ? "tray-tab active" : "tray-tab"}
33
+ onClick={() => setActiveTray?.(tray.id)}
34
+ >
35
+ {tray.label}
36
+ <span className="tray-count">{tray.count}</span>
37
+ </button>
38
+ ))}
39
+ </Container>
40
+ );
41
+ };
@@ -0,0 +1,52 @@
1
+ import styled from "styled-components";
2
+ import { FontFamily, GlobalColors } from "../../../global-files/variables";
3
+
4
+ export const Container = styled.div`
5
+ display: flex;
6
+ align-items: center;
7
+ gap: 4px;
8
+ padding: 8px 0 0;
9
+
10
+ .tray-tab {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 6px;
14
+ padding: 6px 14px;
15
+ border: none;
16
+ border-bottom: 2px solid transparent;
17
+ background: none;
18
+ cursor: pointer;
19
+ font-family: ${FontFamily.AvenirNext};
20
+ font-size: 14px;
21
+ line-height: 21px;
22
+ letter-spacing: -0.015em;
23
+ color: ${GlobalColors.s4};
24
+
25
+ &:hover {
26
+ color: ${GlobalColors.s5};
27
+ }
28
+
29
+ &.active {
30
+ color: ${GlobalColors.s5};
31
+ font-weight: 600;
32
+ border-bottom-color: ${GlobalColors.color_purple};
33
+ }
34
+ }
35
+
36
+ .tray-count {
37
+ min-width: 20px;
38
+ padding: 0 6px;
39
+ border-radius: 10px;
40
+ background: ${GlobalColors.s2};
41
+ color: ${GlobalColors.s5};
42
+ font-family: ${FontFamily.Lato};
43
+ font-size: 11px;
44
+ line-height: 18px;
45
+ text-align: center;
46
+ }
47
+
48
+ .active .tray-count {
49
+ background: ${GlobalColors.color_purple};
50
+ color: ${GlobalColors.white};
51
+ }
52
+ `;
@@ -13,6 +13,7 @@ export const ImageSelector = ({
13
13
  key={index}
14
14
  img={image.src}
15
15
  altText={image.altText}
16
+ ext={image.ext}
16
17
  imageType={modalSelector ? "catalogue-modal-image" : "carousel-image"}
17
18
  onClick={() => setActiveImage(index)}
18
19
  />
@@ -0,0 +1,101 @@
1
+ import { useReducer, useState } from "react";
2
+
3
+ import { DigitalAssetsTray } from "./index";
4
+ import { useDigitalAssets } from "../../../global-files/digital-assets/useDigitalAssets";
5
+ import {
6
+ assetsInitialState,
7
+ assetsReducer,
8
+ countUnsavedAssets,
9
+ hasUnsavedAssets,
10
+ selectAssetsToSave,
11
+ } from "../../../global-files/digital-assets/assets.reducer";
12
+ import { buildSavePayload } from "../../../global-files/digital-assets/assets.adapter";
13
+ import { backendAssetsMock } from "../../../global-files/digital-assets/assets.mock";
14
+
15
+ export default {
16
+ title: "Components/organisms/DigitalAssetsTray",
17
+ component: DigitalAssetsTray,
18
+ };
19
+
20
+ /*
21
+ Las historias usan EL MISMO reducer que la ficha, no un estado de juguete.
22
+ Así lo que se ve aquí es el comportamiento real: qué queda marcado para
23
+ guardar, qué se anota para borrar y qué payload saldría hacia el backend.
24
+ */
25
+ const Demo = ({ initialAssets = [], gridLayout = false, readOnly = false, showPayload = false }) => {
26
+ const [state, dispatch] = useReducer(assetsReducer, assetsInitialState);
27
+ const [notices, setNotices] = useState([]);
28
+
29
+ const tray = useDigitalAssets({
30
+ assets: state.assets_values,
31
+ dispatch,
32
+ notices,
33
+ setNotices,
34
+ });
35
+
36
+ // Carga inicial simulando la respuesta del backend, una sola vez.
37
+ const [loaded, setLoaded] = useState(false);
38
+ if (!loaded && initialAssets.length) {
39
+ tray.replaceAll(initialAssets);
40
+ setLoaded(true);
41
+ }
42
+
43
+ const payload = buildSavePayload(selectAssetsToSave(state));
44
+
45
+ return (
46
+ <div style={{ width: 960 }}>
47
+ <DigitalAssetsTray
48
+ {...tray}
49
+ unsaved={countUnsavedAssets(state)}
50
+ gridLayout={gridLayout}
51
+ readOnly={readOnly}
52
+ selectedAssets={state.selected_assets}
53
+ setSelectedAssets={(assets) => dispatch({ type: "SET_SELECTED_ASSETS", payload: assets })}
54
+ setCheckAll={(checked) => dispatch({ type: "TOGGLE_CHECK_ALL_ASSETS", payload: checked })}
55
+ />
56
+
57
+ <p style={{ fontFamily: "Lato", fontSize: 12 }}>
58
+ Seleccionados: {state.selected_assets.length} · Sin subir: {tray.pending.length} ·
59
+ Marcados para actualizar: {state.updated_assets.length} · Para borrar:{" "}
60
+ {state.deleted_assets.length} · ¿Hay cambios sin guardar?{" "}
61
+ {hasUnsavedAssets(state) ? "sí" : "no"}
62
+ </p>
63
+
64
+ {showPayload && (
65
+ <pre
66
+ style={{
67
+ fontSize: 11,
68
+ background: "#f7f7fc",
69
+ padding: 12,
70
+ borderRadius: 6,
71
+ maxHeight: 280,
72
+ overflow: "auto",
73
+ }}
74
+ >
75
+ {JSON.stringify(payload, null, 2)}
76
+ </pre>
77
+ )}
78
+ </div>
79
+ );
80
+ };
81
+
82
+ /**
83
+ * Estado real de hoy: sin backend, la bandeja arranca vacía. Arrastrar un mp4,
84
+ * un gif, un PDF, un xlsx o un psd lo valida y lo agrega; cualquier otra cosa se
85
+ * rechaza con su motivo. Nada se sube.
86
+ */
87
+ export const Vacia = () => <Demo />;
88
+
89
+ export const ConActivos = () => <Demo initialAssets={backendAssetsMock} />;
90
+
91
+ export const Cuadricula = () => <Demo initialAssets={backendAssetsMock} gridLayout />;
92
+
93
+ export const SoloLectura = () => <Demo initialAssets={backendAssetsMock} readOnly />;
94
+
95
+ /**
96
+ * Lo que se le enviaría al backend, actualizándose en vivo. Cambia el tipo de un
97
+ * activo y aparece en `updateAssets`; bórralo y su id pasa a `deleteAssets`;
98
+ * suelta un archivo nuevo y aparece en `newAssets` sin `key`, porque todavía no
99
+ * se ha subido a S3.
100
+ */
101
+ export const PayloadDeGuardado = () => <Demo initialAssets={backendAssetsMock} showPayload />;