limbo-component 1.6.7 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/limbo.es.js CHANGED
@@ -31,7 +31,7 @@ const DEFAULT_MESSAGES = {
31
31
  "message.no_images": "No hay imágenes disponibles",
32
32
  "message.upload_success": "Imagen subida correctamente",
33
33
  "message.upload_error": "Error al subir la imagen",
34
- "message.delete_confirm": "¿Estás seguro de que quieres eliminar esta imagen?",
34
+ "message.delete_confirm": "¿Estás seguro de que deseas eliminar esta imagen? Esta acción también eliminará todos sus recortes.",
35
35
  "message.copy_success": "URL copiada al portapapeles",
36
36
  "message.copy_error": "Error al copiar URL",
37
37
  // Errores
@@ -219,17 +219,70 @@ class ConfigManager {
219
219
  // libre por defecto
220
220
  lockAspectRatio: false,
221
221
  // Aspect ratios permitidos - formato: { label, value, ratio }
222
- allowedAspectRatios: [
223
- { label: "📐 Libre", value: "", ratio: null },
224
- { label: "⬜ 1:1 (Cuadrado)", value: "1", ratio: 1 },
225
- { label: "📺 4:3 (Clásico)", value: "4/3", ratio: 4 / 3 },
226
- { label: "🖥️ 16:9 (Widescreen)", value: "16/9", ratio: 16 / 9 },
227
- { label: "📷 3:2 (Foto)", value: "3/2", ratio: 3 / 2 },
228
- { label: "📱 2:3 (Retrato)", value: "2/3", ratio: 2 / 3 },
229
- { label: "📲 9:16 (Stories)", value: "9/16", ratio: 9 / 16 }
230
- ],
231
- showFreeAspectRatio: true
222
+ showFreeAspectRatio: true,
232
223
  // Permitir aspecto libre
224
+ // Nuevas opciones avanzadas
225
+ showDimensionInputs: false,
226
+ // Si true, muestra inputs width x height en lugar de aspect ratio
227
+ enableEditMode: true,
228
+ // Si permite editar variantes existentes
229
+ showDownloadInCropper: false,
230
+ // Si muestra botón de descarga en el cropper
231
+ showCropName: false,
232
+ // Si muestra campo de nombre de recorte editable
233
+ // Recortes obligatorios - Array de objetos con {label, width, height, required}
234
+ mandatoryCrops: [],
235
+ /* Ejemplo de mandatoryCrops:
236
+ [
237
+ { label: "Thumbnail", width: 300, height: 300, required: true },
238
+ { label: "Header", width: 1920, height: 400, required: true },
239
+ { label: "Mobile", width: 750, height: 1334, required: false }
240
+ ]
241
+ */
242
+ allowCustomCrops: true
243
+ // Permitir que el usuario añada recortes personalizados además de los obligatorios
244
+ },
245
+ // Configuración de galería
246
+ gallery: {
247
+ // Configuración de filtros
248
+ filters: {
249
+ showNameFilter: true,
250
+ // Mostrar filtro por nombre de archivo
251
+ showUploadedByFilter: false,
252
+ // Mostrar filtro por usuario que subió (oculto por defecto)
253
+ showDateFilter: true,
254
+ // Mostrar filtros de rango de fechas
255
+ enabledFilters: ["name", "date"],
256
+ // Filtros activos por defecto
257
+ // Filtros personalizados adicionales
258
+ customFilters: []
259
+ /* Ejemplo de customFilters:
260
+ [
261
+ {
262
+ key: "tag",
263
+ label: "Etiqueta",
264
+ type: "select", // select | text | date | range
265
+ options: ["producto", "banner", "avatar"]
266
+ }
267
+ ]
268
+ */
269
+ },
270
+ // Configuración de carga
271
+ loading: {
272
+ showPlaceholders: true,
273
+ // Mostrar placeholders mientras carga
274
+ placeholderCount: 10,
275
+ // Número de placeholders a mostrar
276
+ showSpinner: true
277
+ // Mostrar spinner de carga
278
+ },
279
+ // Configuración de paginación
280
+ pagination: {
281
+ itemsPerPage: 20,
282
+ // Elementos por página
283
+ showPageSize: true
284
+ // Permitir cambiar el tamaño de página
285
+ }
233
286
  },
234
287
  // Validación de archivos
235
288
  validation: {
@@ -240,6 +293,9 @@ class ConfigManager {
240
293
  maxWidth: null,
241
294
  maxHeight: null
242
295
  },
296
+ // Configuración de descarga
297
+ downloadFormat: "webp",
298
+ // webp | jpeg | jpg | png - Format for downloaded images
243
299
  // Configuración de retorno
244
300
  return: {
245
301
  format: "url",
@@ -290,9 +346,33 @@ class ConfigManager {
290
346
  showCloseButton: true
291
347
  },
292
348
  // Callbacks por defecto
293
- callbacks: {},
349
+ callbacks: {
350
+ // Callback cuando se sube una imagen
351
+ onUpload: null,
352
+ // (data) => { assetId, url, fileName, mime, width, height, instanceId }
353
+ // Callback cuando se selecciona una imagen
354
+ onSelect: null,
355
+ // (data) => { assetId, url, fileName, mime, width, height, instanceId }
356
+ // Callback cuando se elimina una imagen
357
+ onDelete: null,
358
+ // (assetId, instanceId) => void
359
+ // NUEVO: Callback cuando se guardan recortes/variantes
360
+ onCropsSaved: null,
361
+ // (data) => { crops: Array, assetId, instanceId }
362
+ // NUEVO: Callback cuando se completa el cropper (modo crop-only)
363
+ onCropperComplete: null,
364
+ // (data) => { crops: Array, instanceId }
365
+ // NUEVO: Callback cuando se cancela el cropper sin guardar
366
+ onCropperCancelled: null,
367
+ // (data) => { assetId?, instanceId }
368
+ // NUEVO: Callback cuando ocurre un error en el cropper
369
+ onCropperError: null
370
+ // (error, instanceId) => void
371
+ },
294
372
  // Configuración avanzada
295
373
  autoDestroy: false,
374
+ autoHideOnComplete: false,
375
+ // Si true, oculta el componente al completar (solo embed mode)
296
376
  debug: false,
297
377
  apiEndpoint: null,
298
378
  // ========== CONFIGURACIÓN DE AUTENTICACIÓN JWT V2 ==========
@@ -453,17 +533,34 @@ class ConfigManager {
453
533
  aspectRatio: null,
454
534
  lockAspectRatio: false,
455
535
  // Aspect ratios permitidos - formato: { label, value, ratio }
456
- allowedAspectRatios: [
457
- { label: "📐 Libre", value: "", ratio: null },
458
- { label: "⬜ 1:1 (Cuadrado)", value: "1", ratio: 1 },
459
- { label: "📺 4:3 (Clásico)", value: "4/3", ratio: 4 / 3 },
460
- { label: "🖥️ 16:9 (Widescreen)", value: "16/9", ratio: 16 / 9 },
461
- { label: "📷 3:2 (Foto)", value: "3/2", ratio: 3 / 2 },
462
- { label: "📱 2:3 (Retrato)", value: "2/3", ratio: 2 / 3 },
463
- { label: "📲 9:16 (Stories)", value: "9/16", ratio: 9 / 16 }
464
- ],
465
- showFreeAspectRatio: true
536
+ showFreeAspectRatio: true,
466
537
  // Permitir aspecto libre
538
+ // Nuevas opciones avanzadas
539
+ showDimensionInputs: false,
540
+ enableEditMode: true,
541
+ showDownloadInCropper: false,
542
+ showCropName: false,
543
+ mandatoryCrops: [],
544
+ allowCustomCrops: true
545
+ },
546
+ // Configuración de galería
547
+ gallery: {
548
+ filters: {
549
+ showNameFilter: true,
550
+ showUploadedByFilter: false,
551
+ showDateFilter: true,
552
+ enabledFilters: ["name", "date"],
553
+ customFilters: []
554
+ },
555
+ loading: {
556
+ showPlaceholders: true,
557
+ placeholderCount: 10,
558
+ showSpinner: true
559
+ },
560
+ pagination: {
561
+ itemsPerPage: 20,
562
+ showPageSize: true
563
+ }
467
564
  },
468
565
  // Validación de archivos
469
566
  validation: {
@@ -517,9 +614,33 @@ class ConfigManager {
517
614
  customProperties: {}
518
615
  },
519
616
  // Callbacks por defecto
520
- callbacks: {},
617
+ callbacks: {
618
+ // Callback cuando se sube una imagen
619
+ onUpload: null,
620
+ // (data) => { assetId, url, fileName, mime, width, height, instanceId }
621
+ // Callback cuando se selecciona una imagen
622
+ onSelect: null,
623
+ // (data) => { assetId, url, fileName, mime, width, height, instanceId }
624
+ // Callback cuando se elimina una imagen
625
+ onDelete: null,
626
+ // (assetId, instanceId) => void
627
+ // NUEVO: Callback cuando se guardan recortes/variantes
628
+ onCropsSaved: null,
629
+ // (data) => { crops: Array, assetId, instanceId }
630
+ // NUEVO: Callback cuando se completa el cropper (modo crop-only)
631
+ onCropperComplete: null,
632
+ // (data) => { crops: Array, instanceId }
633
+ // NUEVO: Callback cuando se cancela el cropper sin guardar
634
+ onCropperCancelled: null,
635
+ // (data) => { assetId?, instanceId }
636
+ // NUEVO: Callback cuando ocurre un error en el cropper
637
+ onCropperError: null
638
+ // (error, instanceId) => void
639
+ },
521
640
  // Configuración avanzada
522
641
  autoDestroy: false,
642
+ autoHideOnComplete: false,
643
+ // Si true, oculta el componente al completar (solo embed mode)
523
644
  debug: false,
524
645
  apiEndpoint: null,
525
646
  // ========== CONFIGURACIÓN DE AUTENTICACIÓN JWT V2 ==========
@@ -726,6 +847,128 @@ class ConfigManager {
726
847
  this.setJWTAuth(apiKey);
727
848
  return this;
728
849
  }
850
+ /**
851
+ * ========================================
852
+ * HELPERS PARA CONFIGURACIÓN DE CROPPER
853
+ * ========================================
854
+ */
855
+ /**
856
+ * Obtener recortes obligatorios configurados
857
+ */
858
+ getMandatoryCrops(config = null) {
859
+ const mergedConfig = config || this.merge();
860
+ return mergedConfig.cropper?.mandatoryCrops || [];
861
+ }
862
+ /**
863
+ * Verificar si hay recortes obligatorios configurados
864
+ */
865
+ hasMandatoryCrops(config = null) {
866
+ const crops = this.getMandatoryCrops(config);
867
+ return crops.length > 0;
868
+ }
869
+ /**
870
+ * Verificar si está habilitado el modo de edición de variantes
871
+ */
872
+ isEditModeEnabled(config = null) {
873
+ const mergedConfig = config || this.merge();
874
+ return mergedConfig.cropper?.enableEditMode !== false;
875
+ }
876
+ /**
877
+ * Verificar si se muestran inputs de dimensiones en lugar de aspect ratio
878
+ */
879
+ showDimensionInputs(config = null) {
880
+ const mergedConfig = config || this.merge();
881
+ return mergedConfig.cropper?.showDimensionInputs === true;
882
+ }
883
+ /**
884
+ * Verificar si se muestra botón de descarga en el cropper
885
+ */
886
+ showDownloadInCropper(config = null) {
887
+ const mergedConfig = config || this.merge();
888
+ return mergedConfig.cropper?.showDownloadInCropper === true;
889
+ }
890
+ /**
891
+ * Verificar si se muestra campo de nombre de recorte
892
+ */
893
+ showCropName(config = null) {
894
+ const mergedConfig = config || this.merge();
895
+ return mergedConfig.cropper?.showCropName === true;
896
+ }
897
+ /**
898
+ * Verificar si se permiten recortes personalizados
899
+ */
900
+ allowCustomCrops(config = null) {
901
+ const mergedConfig = config || this.merge();
902
+ return mergedConfig.cropper?.allowCustomCrops !== false;
903
+ }
904
+ /**
905
+ * ========================================
906
+ * HELPERS PARA CONFIGURACIÓN DE GALERÍA
907
+ * ========================================
908
+ */
909
+ /**
910
+ * Obtener configuración de filtros de galería
911
+ */
912
+ getGalleryFilters(config = null) {
913
+ const mergedConfig = config || this.merge();
914
+ return mergedConfig.gallery?.filters || this.defaults.gallery.filters;
915
+ }
916
+ /**
917
+ * Verificar si un filtro específico está habilitado
918
+ */
919
+ isGalleryFilterEnabled(filterName, config = null) {
920
+ const filters = this.getGalleryFilters(config);
921
+ return filters.enabledFilters?.includes(filterName) || false;
922
+ }
923
+ /**
924
+ * Obtener filtros personalizados de galería
925
+ */
926
+ getCustomGalleryFilters(config = null) {
927
+ const filters = this.getGalleryFilters(config);
928
+ return filters.customFilters || [];
929
+ }
930
+ /**
931
+ * Obtener configuración de carga de galería
932
+ */
933
+ getGalleryLoadingConfig(config = null) {
934
+ const mergedConfig = config || this.merge();
935
+ return mergedConfig.gallery?.loading || this.defaults.gallery.loading;
936
+ }
937
+ /**
938
+ * Verificar si se muestran placeholders en la galería
939
+ */
940
+ showGalleryPlaceholders(config = null) {
941
+ const loading = this.getGalleryLoadingConfig(config);
942
+ return loading.showPlaceholders !== false;
943
+ }
944
+ /**
945
+ * Obtener número de placeholders a mostrar
946
+ */
947
+ getPlaceholderCount(config = null) {
948
+ const loading = this.getGalleryLoadingConfig(config);
949
+ return loading.placeholderCount || 10;
950
+ }
951
+ /**
952
+ * Verificar si se muestra spinner de carga en galería
953
+ */
954
+ showGallerySpinner(config = null) {
955
+ const loading = this.getGalleryLoadingConfig(config);
956
+ return loading.showSpinner !== false;
957
+ }
958
+ /**
959
+ * Obtener configuración de paginación de galería
960
+ */
961
+ getGalleryPagination(config = null) {
962
+ const mergedConfig = config || this.merge();
963
+ return mergedConfig.gallery?.pagination || this.defaults.gallery.pagination;
964
+ }
965
+ /**
966
+ * Obtener items por página
967
+ */
968
+ getItemsPerPage(config = null) {
969
+ const pagination = this.getGalleryPagination(config);
970
+ return pagination.itemsPerPage || 20;
971
+ }
729
972
  }
730
973
  var client = { exports: {} };
731
974
  var reactDomClient_production = {};
@@ -31017,37 +31260,33 @@ function ImageVariantsModal({
31017
31260
  parent_asset_id: image.id,
31018
31261
  variant_info: variant
31019
31262
  };
31020
- accessibilityManager?.announce(`Variante seleccionada: ${variant.name}`);
31263
+ accessibilityManager?.announce(`Recorte seleccionado: ${variant.name}`);
31021
31264
  onSelect?.(variantAsImage);
31022
31265
  onClose?.();
31023
31266
  };
31024
31267
  const handleCopyVariantUrl = async (variant) => {
31025
31268
  try {
31026
31269
  await navigator.clipboard.writeText(variant.url);
31027
- accessibilityManager?.announce(`URL de variante ${variant.name} copiada`);
31270
+ accessibilityManager?.announce(`URL de recorte ${variant.name} copiada`);
31028
31271
  } catch (err) {
31029
31272
  console.error("Error copying variant URL:", err);
31030
- accessibilityManager?.announceError("Error al copiar URL de variante");
31273
+ accessibilityManager?.announceError("Error al copiar URL de recorte");
31031
31274
  }
31032
31275
  };
31033
- const handleDownloadVariant = (variant) => {
31034
- accessibilityManager?.announce(`Descargando ${image.filename}`);
31035
- fetch(image.url || image.path, { mode: "cors" }).then((resp) => resp.blob()).then((blob) => {
31036
- const url = window.URL.createObjectURL(blob);
31037
- const a = document.createElement("a");
31038
- a.href = url;
31039
- a.download = `${variant.name}.${variant.format}`;
31040
- document.body.appendChild(a);
31041
- a.click();
31042
- setTimeout(() => {
31043
- window.URL.revokeObjectURL(url);
31044
- document.body.removeChild(a);
31045
- }, 100);
31046
- });
31276
+ const handleDownloadVariant = async (variant) => {
31277
+ const { downloadImage: downloadImage2 } = await Promise.resolve().then(() => downloadImage$1);
31278
+ await downloadImage2(
31279
+ variant.url,
31280
+ variant.name || "limbo-variant",
31281
+ {
31282
+ originalFormat: variant.format || variant.output_format,
31283
+ accessibilityManager
31284
+ }
31285
+ );
31047
31286
  };
31048
31287
  const handleViewVariant = (variant) => {
31049
31288
  accessibilityManager?.announce(
31050
- `Abriendo variante ${variant.name} en nueva pestaña`
31289
+ `Abriendo recorte ${variant.name} en nueva pestaña`
31051
31290
  );
31052
31291
  window.open(variant.url, "_blank");
31053
31292
  };
@@ -31066,26 +31305,21 @@ function ImageVariantsModal({
31066
31305
  accessibilityManager?.announceError("Error al copiar URL");
31067
31306
  }
31068
31307
  };
31069
- const handleDownloadOriginal = () => {
31070
- accessibilityManager?.announce(`Descargando ${image.filename}`);
31071
- fetch(image.url || image.path, { mode: "cors" }).then((resp) => resp.blob()).then((blob) => {
31072
- const url = window.URL.createObjectURL(blob);
31073
- const a = document.createElement("a");
31074
- const filename = image.filename.split(".")[0];
31075
- a.href = url;
31076
- a.download = `${filename || "image"}.webp`;
31077
- document.body.appendChild(a);
31078
- a.click();
31079
- setTimeout(() => {
31080
- window.URL.revokeObjectURL(url);
31081
- document.body.removeChild(a);
31082
- }, 100);
31083
- });
31308
+ const handleDownloadOriginal = async () => {
31309
+ const { downloadImage: downloadImage2 } = await Promise.resolve().then(() => downloadImage$1);
31310
+ await downloadImage2(
31311
+ image.url || image.path,
31312
+ image.filename || "limbo-image",
31313
+ {
31314
+ originalFormat: image.format,
31315
+ accessibilityManager
31316
+ }
31317
+ );
31084
31318
  };
31085
31319
  const handleDeleteOriginal = async () => {
31086
31320
  if (!onDelete) return;
31087
31321
  const confirmed = window.confirm(
31088
- `¿Estás seguro de que deseas eliminar "${image.filename}"? Esta acción también eliminará todas sus variantes.`
31322
+ `¿Estás seguro de que deseas eliminar "${image.filename}"? Esta acción también eliminará todos sus recortes.`
31089
31323
  );
31090
31324
  if (confirmed) {
31091
31325
  accessibilityManager?.announce(`Eliminando imagen ${image.filename}`);
@@ -31103,27 +31337,27 @@ function ImageVariantsModal({
31103
31337
  };
31104
31338
  const handleDeleteVariant = async (variant) => {
31105
31339
  const confirmed = window.confirm(
31106
- `¿Estás seguro de que deseas eliminar la variante "${variant.name || variant.filename}"?`
31340
+ `¿Estás seguro de que deseas eliminar el recorte "${variant.name || variant.filename}"?`
31107
31341
  );
31108
31342
  if (confirmed) {
31109
31343
  accessibilityManager?.announce(
31110
- `Eliminando variante ${variant.name || variant.filename}`
31344
+ `Eliminando recorte ${variant.name || variant.filename}`
31111
31345
  );
31112
31346
  const result = await removeVariant(image.id, variant.id);
31113
31347
  if (result.success) {
31114
- setDeleteMessage("Variante eliminada correctamente");
31348
+ setDeleteMessage("Recorte eliminado correctamente");
31115
31349
  setDeleteMessageType("success");
31116
- accessibilityManager?.announce(`Variante eliminada correctamente`);
31350
+ accessibilityManager?.announce(`Recorte eliminado correctamente`);
31117
31351
  onVariantDeleted?.();
31118
31352
  setTimeout(() => {
31119
31353
  setDeleteMessage(null);
31120
31354
  setDeleteMessageType(null);
31121
31355
  }, 3e3);
31122
31356
  } else {
31123
- setDeleteMessage(`Error al eliminar variante: ${result.error}`);
31357
+ setDeleteMessage(`Error al eliminar recorte: ${result.error}`);
31124
31358
  setDeleteMessageType("error");
31125
31359
  accessibilityManager?.announceError(
31126
- `Error al eliminar variante: ${result.error}`
31360
+ `Error al eliminar recorte: ${result.error}`
31127
31361
  );
31128
31362
  setTimeout(() => {
31129
31363
  setDeleteMessage(null);
@@ -31148,7 +31382,7 @@ function ImageVariantsModal({
31148
31382
  children: [
31149
31383
  /* @__PURE__ */ jsxs("div", { className: "limbo-modal-header", children: [
31150
31384
  /* @__PURE__ */ jsxs("h2", { id: "variants-modal-title", children: [
31151
- "Variantes de ",
31385
+ "Recortes de ",
31152
31386
  image?.filename
31153
31387
  ] }),
31154
31388
  /* @__PURE__ */ jsx(
@@ -31156,7 +31390,7 @@ function ImageVariantsModal({
31156
31390
  {
31157
31391
  className: "limbo-modal-close",
31158
31392
  onClick: onClose,
31159
- "aria-label": "Cerrar modal de variantes",
31393
+ "aria-label": "Cerrar modal de recortes",
31160
31394
  children: "✕"
31161
31395
  }
31162
31396
  )
@@ -31179,10 +31413,10 @@ function ImageVariantsModal({
31179
31413
  ),
31180
31414
  /* @__PURE__ */ jsx("div", { className: "limbo-modal-body", children: loading ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-loading", children: [
31181
31415
  /* @__PURE__ */ jsx("div", { className: "limbo-loader" }),
31182
- /* @__PURE__ */ jsx("p", { children: "Cargando variantes..." })
31416
+ /* @__PURE__ */ jsx("p", { children: "Cargando recortes..." })
31183
31417
  ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-error", children: [
31184
31418
  /* @__PURE__ */ jsxs("p", { children: [
31185
- "Error al cargar variantes: ",
31419
+ "Error al cargar recortes: ",
31186
31420
  error
31187
31421
  ] }),
31188
31422
  /* @__PURE__ */ jsx(
@@ -31194,8 +31428,8 @@ function ImageVariantsModal({
31194
31428
  }
31195
31429
  )
31196
31430
  ] }) : variants.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-empty", children: [
31197
- /* @__PURE__ */ jsx("p", { children: "Esta imagen no tiene variantes aún." }),
31198
- /* @__PURE__ */ jsx("small", { children: "Las variantes aparecerán aquí después de hacer recortes o redimensionados." })
31431
+ /* @__PURE__ */ jsx("p", { children: "Esta imagen no tiene recortes aún." }),
31432
+ /* @__PURE__ */ jsx("small", { children: "Los recortes aparecerán aquí después de hacer recortes o redimensionados." })
31199
31433
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
31200
31434
  /* @__PURE__ */ jsxs("div", { className: "limbo-variant-section", children: [
31201
31435
  /* @__PURE__ */ jsx("h3", { children: "Imagen original" }),
@@ -31289,7 +31523,7 @@ function ImageVariantsModal({
31289
31523
  ] }),
31290
31524
  /* @__PURE__ */ jsxs("div", { className: "limbo-variant-section", children: [
31291
31525
  /* @__PURE__ */ jsxs("h3", { children: [
31292
- "Variantes (",
31526
+ "Recortes (",
31293
31527
  variants.length,
31294
31528
  ")"
31295
31529
  ] }),
@@ -31428,7 +31662,7 @@ function ImageCard({
31428
31662
  delete: true,
31429
31663
  crop: true,
31430
31664
  variants: true
31431
- // Nueva acción para ver variantes
31665
+ // Nueva acción para ver recortes
31432
31666
  }
31433
31667
  }) {
31434
31668
  const [copied, setCopied] = useState(false);
@@ -31483,23 +31717,18 @@ function ImageCard({
31483
31717
  );
31484
31718
  }
31485
31719
  };
31486
- const handleDownload = (e) => {
31720
+ const handleDownload = async (e) => {
31487
31721
  e.preventDefault();
31488
31722
  e.stopPropagation();
31489
- accessibilityManager?.announce(`Descargando ${image.filename}`);
31490
- fetch(image.url || image.path, { mode: "cors" }).then((resp) => resp.blob()).then((blob) => {
31491
- const url = window.URL.createObjectURL(blob);
31492
- const a = document.createElement("a");
31493
- const filename = image.filename.split(".")[0];
31494
- a.href = url;
31495
- a.download = `${filename || "image"}.webp`;
31496
- document.body.appendChild(a);
31497
- a.click();
31498
- setTimeout(() => {
31499
- window.URL.revokeObjectURL(url);
31500
- document.body.removeChild(a);
31501
- }, 100);
31502
- });
31723
+ const { downloadImage: downloadImage2 } = await Promise.resolve().then(() => downloadImage$1);
31724
+ await downloadImage2(
31725
+ image.url || image.path,
31726
+ image.filename || "limbo-image",
31727
+ {
31728
+ originalFormat: image.format,
31729
+ accessibilityManager
31730
+ }
31731
+ );
31503
31732
  };
31504
31733
  const handleCrop = (e) => {
31505
31734
  e.stopPropagation();
@@ -31516,7 +31745,7 @@ function ImageCard({
31516
31745
  }, [image.variants_count]);
31517
31746
  const handleShowVariants = (e) => {
31518
31747
  e.stopPropagation();
31519
- accessibilityManager?.announce(`Mostrando variantes de ${image.filename}`);
31748
+ accessibilityManager?.announce(`Mostrando recortes de ${image.filename}`);
31520
31749
  setShowVariants(true);
31521
31750
  };
31522
31751
  const handleSelect = (e) => {
@@ -31604,7 +31833,7 @@ function ImageCard({
31604
31833
  "button",
31605
31834
  {
31606
31835
  type: "button",
31607
- title: `Ver ${variantsCount} variante${variantsCount !== 1 ? "s" : ""}`,
31836
+ title: `Ver ${variantsCount} recorte${variantsCount !== 1 ? "s" : ""}`,
31608
31837
  className: `btn btn-variants border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31609
31838
  onClick: handleShowVariants,
31610
31839
  tabIndex: -1,
@@ -31775,7 +32004,7 @@ function ImageCard({
31775
32004
  {
31776
32005
  src: image.url || image.path,
31777
32006
  loading: "eager",
31778
- alt: image.filename,
32007
+ alt: image.filename.split(".")[0] ?? image.filename,
31779
32008
  className: "w-full object-cover rounded aspect-square",
31780
32009
  sizes: `height: ${thumbnailSize * 6}px,width: ${thumbnailSize * 6}px`,
31781
32010
  draggable: false,
@@ -31807,7 +32036,7 @@ function ImageCard({
31807
32036
  fontWeight: "500"
31808
32037
  }
31809
32038
  },
31810
- children: image.filename
32039
+ children: image.filename.split(".")[0] ?? image.filename
31811
32040
  }
31812
32041
  )
31813
32042
  ]
@@ -31834,6 +32063,38 @@ function ImageCard({
31834
32063
  )
31835
32064
  ] });
31836
32065
  }
32066
+ function ImageCardSkeleton() {
32067
+ return /* @__PURE__ */ jsxs(
32068
+ "div",
32069
+ {
32070
+ className: "limbo-image-card animate-pulse",
32071
+ role: "status",
32072
+ "aria-label": "Cargando imagen...",
32073
+ children: [
32074
+ /* @__PURE__ */ jsx("div", { className: "w-full aspect-square bg-neutral-gray-200 rounded-md flex items-center justify-center", children: /* @__PURE__ */ jsx(
32075
+ "svg",
32076
+ {
32077
+ className: "w-12 h-12 text-neutral-gray-300",
32078
+ fill: "currentColor",
32079
+ viewBox: "0 0 20 20",
32080
+ xmlns: "http://www.w3.org/2000/svg",
32081
+ "aria-hidden": "true",
32082
+ children: /* @__PURE__ */ jsx(
32083
+ "path",
32084
+ {
32085
+ fillRule: "evenodd",
32086
+ d: "M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z",
32087
+ clipRule: "evenodd"
32088
+ }
32089
+ )
32090
+ }
32091
+ ) }),
32092
+ /* @__PURE__ */ jsx("div", { className: "limbo-image-card-name opacity-100 position-relative bottom-0 p-2", children: /* @__PURE__ */ jsx("div", { className: "h-4 bg-neutral-gray-200 rounded w-3/4" }) }),
32093
+ /* @__PURE__ */ jsx("span", { className: "limbo-sr-only", children: "Cargando imagen..." })
32094
+ ]
32095
+ }
32096
+ );
32097
+ }
31837
32098
  function Loader({ text = "Cargando..." }) {
31838
32099
  return /* @__PURE__ */ jsxs(
31839
32100
  "div",
@@ -31879,50 +32140,7 @@ function Loader({ text = "Cargando..." }) {
31879
32140
  }
31880
32141
  );
31881
32142
  }
31882
- const cache$5 = /* @__PURE__ */ new Map();
31883
- const CACHE_TTL$5 = 5 * 60 * 1e3;
31884
- function useIsAllowedAll() {
31885
- const [allowed, setAllowed] = useState(null);
31886
- const [loading, setLoading] = useState(true);
31887
- const [error, setError] = useState(null);
31888
- useEffect(() => {
31889
- const cacheKey = "auth-status";
31890
- const cached = cache$5.get(cacheKey);
31891
- const now = Date.now();
31892
- if (cached && now - cached.timestamp < CACHE_TTL$5) {
31893
- setAllowed(cached.data);
31894
- setLoading(false);
31895
- return;
31896
- }
31897
- let isMounted = true;
31898
- const checkAuthStatus = async () => {
31899
- try {
31900
- const isAuthenticated = true;
31901
- if (!isMounted) return;
31902
- cache$5.set(cacheKey, { data: isAuthenticated, timestamp: Date.now() });
31903
- setAllowed(isAuthenticated);
31904
- } catch (err) {
31905
- if (isMounted) {
31906
- setError(err.message);
31907
- setAllowed(false);
31908
- }
31909
- } finally {
31910
- if (isMounted) setLoading(false);
31911
- }
31912
- };
31913
- checkAuthStatus();
31914
- return () => {
31915
- isMounted = false;
31916
- };
31917
- }, []);
31918
- const invalidateCache = () => {
31919
- cache$5.delete("auth-status");
31920
- };
31921
- return { allowed, loading, error, invalidateCache };
31922
- }
31923
32143
  function Gallery({
31924
- apiKey,
31925
- prod,
31926
32144
  onSelect,
31927
32145
  onCrop,
31928
32146
  // Nueva prop separada para cropping
@@ -31931,6 +32149,24 @@ function Gallery({
31931
32149
  images,
31932
32150
  loading,
31933
32151
  error,
32152
+ filters = {
32153
+ name: "",
32154
+ dateFrom: "",
32155
+ dateTo: "",
32156
+ uploadedBy: ""
32157
+ },
32158
+ onFiltersChange,
32159
+ filterConfig = {
32160
+ showNameFilter: true,
32161
+ showUploadedByFilter: false,
32162
+ // Oculto por defecto según especificaciones
32163
+ showDateFilter: true
32164
+ },
32165
+ loadingConfig = {
32166
+ showPlaceholders: true,
32167
+ placeholderCount: 10,
32168
+ showSpinner: true
32169
+ },
31934
32170
  allowedActions = {
31935
32171
  select: true,
31936
32172
  download: true,
@@ -31938,18 +32174,13 @@ function Gallery({
31938
32174
  delete: true,
31939
32175
  crop: true,
31940
32176
  variants: true
31941
- // Nueva acción para ver variantes
32177
+ // Nueva acción para ver recortes
31942
32178
  }
31943
32179
  }) {
31944
- const [filters, setFilters] = useState({
31945
- search: "",
31946
- dateFrom: "",
31947
- dateTo: "",
31948
- originPortal: "",
31949
- uploadedBy: ""
31950
- });
32180
+ const showNameFilter = filterConfig.showNameFilter !== false;
32181
+ const showUploadedByFilter = filterConfig.showUploadedByFilter === true;
32182
+ const showDateFilter = filterConfig.showDateFilter !== false;
31951
32183
  const galleryRef = useRef(null);
31952
- const showOriginPortal = useIsAllowedAll();
31953
32184
  const accessibilityManager = window.limboCore?.accessibilityManager;
31954
32185
  useEffect(() => {
31955
32186
  if (galleryRef.current && window.limboCore?.keyboardManager) {
@@ -31976,7 +32207,9 @@ function Gallery({
31976
32207
  }, [loading, error, images.length, accessibilityManager]);
31977
32208
  const handleChange = (e) => {
31978
32209
  const { name, value } = e.target;
31979
- setFilters((prev) => ({ ...prev, [name]: value }));
32210
+ if (onFiltersChange) {
32211
+ onFiltersChange({ ...filters, [name]: value });
32212
+ }
31980
32213
  };
31981
32214
  return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
31982
32215
  /* @__PURE__ */ jsx("div", { className: "limbo-card mb-6 p-4 bg-brand-blue-050 shadow-md", children: /* @__PURE__ */ jsxs(
@@ -31986,16 +32219,16 @@ function Gallery({
31986
32219
  onSubmit: (e) => e.preventDefault(),
31987
32220
  "aria-label": "Filtrar imágenes",
31988
32221
  children: [
31989
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:min-w-[180px] sm:flex-1", children: [
31990
- /* @__PURE__ */ jsx("label", { htmlFor: "search", className: "form-label mb-1", children: "Nombre" }),
32222
+ showNameFilter && /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:min-w-[180px] sm:flex-1", children: [
32223
+ /* @__PURE__ */ jsx("label", { htmlFor: "name", className: "form-label mb-1", children: "Nombre" }),
31991
32224
  /* @__PURE__ */ jsx(
31992
32225
  "input",
31993
32226
  {
31994
32227
  type: "text",
31995
- name: "search",
31996
- id: "search",
32228
+ name: "name",
32229
+ id: "name",
31997
32230
  placeholder: "Buscar por nombre...",
31998
- value: filters.search,
32231
+ value: filters.name,
31999
32232
  onChange: handleChange,
32000
32233
  className: "form-control",
32001
32234
  autoComplete: "off"
@@ -32003,50 +32236,37 @@ function Gallery({
32003
32236
  )
32004
32237
  ] }),
32005
32238
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row flex-wrap gap-2 justify-between items-start sm:items-end w-full sm:w-auto", children: [
32006
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32007
- /* @__PURE__ */ jsx("label", { htmlFor: "dateFrom", className: "form-label mb-1", children: "Desde" }),
32008
- /* @__PURE__ */ jsx(
32009
- "input",
32010
- {
32011
- type: "date",
32012
- name: "dateFrom",
32013
- id: "dateFrom",
32014
- value: filters.dateFrom,
32015
- onChange: handleChange,
32016
- className: "form-control"
32017
- }
32018
- )
32019
- ] }),
32020
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32021
- /* @__PURE__ */ jsx("label", { htmlFor: "dateTo", className: "form-label mb-1", children: "Hasta" }),
32022
- /* @__PURE__ */ jsx(
32023
- "input",
32024
- {
32025
- type: "date",
32026
- name: "dateTo",
32027
- id: "dateTo",
32028
- value: filters.dateTo,
32029
- onChange: handleChange,
32030
- className: "form-control"
32031
- }
32032
- )
32033
- ] }),
32034
- showOriginPortal && /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32035
- /* @__PURE__ */ jsx("label", { htmlFor: "originPortal", className: "form-label mb-1", children: "Portal de origen" }),
32036
- /* @__PURE__ */ jsx(
32037
- "input",
32038
- {
32039
- type: "text",
32040
- name: "originPortal",
32041
- id: "originPortal",
32042
- placeholder: "Portal de origen",
32043
- value: filters.originPortal,
32044
- onChange: handleChange,
32045
- className: "form-control"
32046
- }
32047
- )
32239
+ showDateFilter && /* @__PURE__ */ jsxs(Fragment, { children: [
32240
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32241
+ /* @__PURE__ */ jsx("label", { htmlFor: "dateFrom", className: "form-label mb-1", children: "Desde" }),
32242
+ /* @__PURE__ */ jsx(
32243
+ "input",
32244
+ {
32245
+ type: "date",
32246
+ name: "dateFrom",
32247
+ id: "dateFrom",
32248
+ value: filters.dateFrom,
32249
+ onChange: handleChange,
32250
+ className: "form-control"
32251
+ }
32252
+ )
32253
+ ] }),
32254
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32255
+ /* @__PURE__ */ jsx("label", { htmlFor: "dateTo", className: "form-label mb-1", children: "Hasta" }),
32256
+ /* @__PURE__ */ jsx(
32257
+ "input",
32258
+ {
32259
+ type: "date",
32260
+ name: "dateTo",
32261
+ id: "dateTo",
32262
+ value: filters.dateTo,
32263
+ onChange: handleChange,
32264
+ className: "form-control"
32265
+ }
32266
+ )
32267
+ ] })
32048
32268
  ] }),
32049
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32269
+ showUploadedByFilter && /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full sm:w-auto", children: [
32050
32270
  /* @__PURE__ */ jsx("label", { htmlFor: "uploadedBy", className: "form-label mb-1", children: "Subido por" }),
32051
32271
  /* @__PURE__ */ jsx(
32052
32272
  "input",
@@ -32065,7 +32285,27 @@ function Gallery({
32065
32285
  ]
32066
32286
  }
32067
32287
  ) }),
32068
- loading ? /* @__PURE__ */ jsx(Loader, { text: "Cargando imágenes..." }) : error ? /* @__PURE__ */ jsx("div", { className: "alert alert-danger text-center", children: error }) : /* @__PURE__ */ jsx(
32288
+ loading ? loadingConfig.showPlaceholders ? /* @__PURE__ */ jsx(
32289
+ "div",
32290
+ {
32291
+ ref: galleryRef,
32292
+ className: "limbo-gallery mt-4",
32293
+ "data-limbo-responsive": true,
32294
+ role: "grid",
32295
+ "aria-label": "Cargando imágenes de la galería",
32296
+ "aria-busy": "true",
32297
+ children: Array.from({ length: loadingConfig.placeholderCount }).map((_, index) => /* @__PURE__ */ jsx(
32298
+ "div",
32299
+ {
32300
+ role: "gridcell",
32301
+ "aria-posinset": index + 1,
32302
+ "aria-setsize": loadingConfig.placeholderCount,
32303
+ children: /* @__PURE__ */ jsx(ImageCardSkeleton, {})
32304
+ },
32305
+ `skeleton-${index}`
32306
+ ))
32307
+ }
32308
+ ) : loadingConfig.showSpinner ? /* @__PURE__ */ jsx(Loader, { text: "Cargando imágenes..." }) : null : error ? /* @__PURE__ */ jsx("div", { className: "alert alert-danger text-center", children: error }) : /* @__PURE__ */ jsx(
32069
32309
  "div",
32070
32310
  {
32071
32311
  ref: galleryRef,
@@ -36597,6 +36837,21 @@ class CropperManager {
36597
36837
  return false;
36598
36838
  }
36599
36839
  },
36840
+ // Establecer posición y tamaño directamente
36841
+ set: (x, y, width, height) => {
36842
+ if (!this.selectionElement) return false;
36843
+ try {
36844
+ this.selectionElement.x = x;
36845
+ this.selectionElement.y = y;
36846
+ this.selectionElement.width = width;
36847
+ this.selectionElement.height = height;
36848
+ this.selectionElement.$render();
36849
+ return true;
36850
+ } catch (error) {
36851
+ console.warn("Error setting selection:", error);
36852
+ return false;
36853
+ }
36854
+ },
36600
36855
  // Exportar a canvas
36601
36856
  toCanvas: async (options = {}) => {
36602
36857
  if (!this.selectionElement) return null;
@@ -36998,11 +37253,136 @@ const useCropper = (image, options = {}) => {
36998
37253
  manager: managerRef.current
36999
37254
  };
37000
37255
  };
37256
+ async function downloadImage(imageSource, filename, options = {}) {
37257
+ const {
37258
+ format = null,
37259
+ originalFormat = "webp",
37260
+ onSuccess = null,
37261
+ onError = null,
37262
+ accessibilityManager = null
37263
+ } = options;
37264
+ try {
37265
+ const globalConfig2 = window.limboCore?.config?.getGlobal();
37266
+ const downloadFormat = format || globalConfig2?.downloadFormat || originalFormat || "webp";
37267
+ const cleanFilename = filename.split(".")[0];
37268
+ const finalFilename = `${cleanFilename}.${downloadFormat}`;
37269
+ if (accessibilityManager) {
37270
+ accessibilityManager.announce(`Descargando ${finalFilename}`);
37271
+ }
37272
+ if (imageSource.startsWith("data:image/")) {
37273
+ downloadFromBase64(imageSource, finalFilename);
37274
+ if (onSuccess) {
37275
+ onSuccess(finalFilename);
37276
+ }
37277
+ if (accessibilityManager) {
37278
+ accessibilityManager.announce(`${finalFilename} descargado correctamente`);
37279
+ }
37280
+ return true;
37281
+ }
37282
+ const response = await fetch(imageSource, { mode: "cors" });
37283
+ if (!response.ok) {
37284
+ throw new Error(`HTTP error! status: ${response.status}`);
37285
+ }
37286
+ const blob = await response.blob();
37287
+ let finalBlob = blob;
37288
+ const sourceMimeType = blob.type;
37289
+ const targetMimeType = `image/${downloadFormat === "jpg" ? "jpeg" : downloadFormat}`;
37290
+ if (sourceMimeType !== targetMimeType && shouldConvertFormat(downloadFormat)) {
37291
+ finalBlob = await convertBlobFormat(blob, downloadFormat);
37292
+ }
37293
+ const blobUrl = window.URL.createObjectURL(finalBlob);
37294
+ const link = document.createElement("a");
37295
+ link.href = blobUrl;
37296
+ link.download = finalFilename;
37297
+ link.style.display = "none";
37298
+ document.body.appendChild(link);
37299
+ link.click();
37300
+ setTimeout(() => {
37301
+ window.URL.revokeObjectURL(blobUrl);
37302
+ document.body.removeChild(link);
37303
+ }, 100);
37304
+ if (onSuccess) {
37305
+ onSuccess(finalFilename);
37306
+ }
37307
+ if (accessibilityManager) {
37308
+ accessibilityManager.announce(`${finalFilename} descargado correctamente`);
37309
+ }
37310
+ return true;
37311
+ } catch (error) {
37312
+ console.error("Error downloading image:", error);
37313
+ if (onError) {
37314
+ onError(error);
37315
+ }
37316
+ if (accessibilityManager) {
37317
+ accessibilityManager.announce(`Error al descargar la imagen: ${error.message}`);
37318
+ }
37319
+ return false;
37320
+ }
37321
+ }
37322
+ function downloadFromBase64(dataUri, filename) {
37323
+ const link = document.createElement("a");
37324
+ link.href = dataUri;
37325
+ link.download = filename;
37326
+ link.style.display = "none";
37327
+ document.body.appendChild(link);
37328
+ link.click();
37329
+ setTimeout(() => {
37330
+ document.body.removeChild(link);
37331
+ }, 100);
37332
+ }
37333
+ function shouldConvertFormat(format) {
37334
+ const supportedConversions = ["webp", "jpeg", "jpg", "png"];
37335
+ return supportedConversions.includes(format.toLowerCase());
37336
+ }
37337
+ async function convertBlobFormat(blob, targetFormat) {
37338
+ return new Promise((resolve, reject) => {
37339
+ const img = new Image();
37340
+ const objectUrl = URL.createObjectURL(blob);
37341
+ img.onload = () => {
37342
+ try {
37343
+ const canvas = document.createElement("canvas");
37344
+ canvas.width = img.width;
37345
+ canvas.height = img.height;
37346
+ const ctx = canvas.getContext("2d");
37347
+ ctx.drawImage(img, 0, 0);
37348
+ const mimeType = `image/${targetFormat === "jpg" ? "jpeg" : targetFormat}`;
37349
+ const quality = targetFormat === "png" ? void 0 : 0.92;
37350
+ canvas.toBlob(
37351
+ (convertedBlob) => {
37352
+ URL.revokeObjectURL(objectUrl);
37353
+ if (convertedBlob) {
37354
+ resolve(convertedBlob);
37355
+ } else {
37356
+ resolve(blob);
37357
+ }
37358
+ },
37359
+ mimeType,
37360
+ quality
37361
+ );
37362
+ } catch (error) {
37363
+ URL.revokeObjectURL(objectUrl);
37364
+ resolve(blob);
37365
+ }
37366
+ };
37367
+ img.onerror = () => {
37368
+ URL.revokeObjectURL(objectUrl);
37369
+ resolve(blob);
37370
+ };
37371
+ img.src = objectUrl;
37372
+ });
37373
+ }
37374
+ const downloadImage$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37375
+ __proto__: null,
37376
+ default: downloadImage,
37377
+ downloadImage
37378
+ }, Symbol.toStringTag, { value: "Module" }));
37001
37379
  function CropperView({
37002
37380
  image,
37003
37381
  onSave,
37004
37382
  onCancel,
37005
37383
  onDelete,
37384
+ onError = null,
37385
+ // Callback para manejar errores
37006
37386
  deleting = false,
37007
37387
  onVariantCreated = null
37008
37388
  // Callback cuando se crea una variante
@@ -37010,7 +37390,6 @@ function CropperView({
37010
37390
  const [showPreview, setShowPreview] = useState(false);
37011
37391
  const [previewUrl, setPreviewUrl] = useState(null);
37012
37392
  const [previewLoading, setPreviewLoading] = useState(false);
37013
- const [aspectRatio, setAspectRatio] = useState("");
37014
37393
  const [showGrid, setShowGrid] = useState(true);
37015
37394
  const [shade, setShade] = useState(true);
37016
37395
  const [flipStates, setFlipStates] = useState({
@@ -37018,27 +37397,88 @@ function CropperView({
37018
37397
  vertical: false
37019
37398
  });
37020
37399
  const [showTips, setShowTips] = useState(false);
37400
+ const [showVisualOptions, setShowVisualOptions] = useState(true);
37401
+ const [showSelectorOptions, setShowSelectorOptions] = useState(true);
37402
+ const [showImageOptions, setShowImageOptions] = useState(true);
37021
37403
  const [zoomInfo, setZoomInfo] = useState({ current: 1, percentage: 100 });
37022
- const accessibilityManager = window.limboCore?.accessibilityManager;
37023
- const allowedAspectRatios = useMemo(() => {
37404
+ const [editableFilename] = useState(() => {
37405
+ const [name] = image.filename.split(".");
37406
+ return name;
37407
+ });
37408
+ const cropConfig = useMemo(() => {
37024
37409
  const config = window.limboCore?.config?.getGlobal() || {};
37025
- return config.cropper?.allowedAspectRatios || [
37026
- { label: "📐 Libre", value: "", ratio: null },
37027
- { label: "⬜ 1:1 (Cuadrado)", value: "1", ratio: 1 },
37028
- { label: "📺 4:3 (Clásico)", value: "4/3", ratio: 4 / 3 },
37029
- { label: "🖥️ 16:9 (Widescreen)", value: "16/9", ratio: 16 / 9 },
37030
- { label: "📷 3:2 (Foto)", value: "3/2", ratio: 3 / 2 },
37031
- { label: "📱 2:3 (Retrato)", value: "2/3", ratio: 2 / 3 },
37032
- { label: "📲 9:16 (Stories)", value: "9/16", ratio: 9 / 16 }
37033
- ];
37410
+ const mandatoryCrops = config.cropper?.mandatoryCrops || [];
37411
+ const allowCustomCrops = config.cropper?.allowCustomCrops !== false;
37412
+ const showDimensionInputs = config.cropper?.showDimensionInputs === true;
37413
+ return {
37414
+ mandatoryCrops,
37415
+ allowCustomCrops,
37416
+ showDimensionInputs
37417
+ };
37034
37418
  }, []);
37419
+ const [crops, setCrops] = useState(() => {
37420
+ if (cropConfig.mandatoryCrops.length > 0) {
37421
+ return cropConfig.mandatoryCrops.map((crop, index) => ({
37422
+ id: `crop-${index}`,
37423
+ label: crop.label,
37424
+ width: crop.width,
37425
+ height: crop.height,
37426
+ required: crop.required !== false,
37427
+ // Por defecto son obligatorios
37428
+ isCustom: false,
37429
+ confirmed: false,
37430
+ // Estado del crop (se guarda al cambiar de crop)
37431
+ savedState: null
37432
+ // {cropData, transforms: {zoom, rotation, flip}}
37433
+ }));
37434
+ } else {
37435
+ return [
37436
+ {
37437
+ id: "crop-default-0",
37438
+ label: editableFilename,
37439
+ width: image.width || 1920,
37440
+ height: image.height || 1080,
37441
+ required: false,
37442
+ isCustom: true,
37443
+ confirmed: false,
37444
+ savedState: null
37445
+ }
37446
+ ];
37447
+ }
37448
+ });
37449
+ const [activeCropIndex, setActiveCropIndex] = useState(0);
37450
+ const activeCrop = crops[activeCropIndex];
37451
+ const [shouldCenter, setShouldCenter] = useState(false);
37452
+ const calculatedAspectRatio = useMemo(() => {
37453
+ if (!activeCrop || !activeCrop.width || !activeCrop.height) return "";
37454
+ return activeCrop.width / activeCrop.height;
37455
+ }, [activeCrop]);
37456
+ const minCropSizes = useMemo(() => {
37457
+ const MIN_SIZE = 50;
37458
+ if (!calculatedAspectRatio || calculatedAspectRatio === "") {
37459
+ return { minWidth: MIN_SIZE, minHeight: MIN_SIZE };
37460
+ }
37461
+ if (calculatedAspectRatio >= 1) {
37462
+ const minWidth = MIN_SIZE;
37463
+ const minHeight = MIN_SIZE / calculatedAspectRatio;
37464
+ return { minWidth, minHeight };
37465
+ } else {
37466
+ const minHeight = MIN_SIZE;
37467
+ const minWidth = MIN_SIZE * calculatedAspectRatio;
37468
+ return { minWidth, minHeight };
37469
+ }
37470
+ }, [calculatedAspectRatio]);
37471
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
37472
+ const [selectedCropsForAction, setSelectedCropsForAction] = useState([]);
37473
+ const [pendingAction, setPendingAction] = useState(null);
37474
+ const accessibilityManager = window.limboCore?.accessibilityManager;
37035
37475
  const {
37036
37476
  createCropVariant,
37037
37477
  loading: creatingVariant,
37038
37478
  error: variantError
37039
37479
  } = useCreateVariant();
37040
37480
  const cropper = useCropper(image, {
37041
- aspectRatio,
37481
+ aspectRatio: calculatedAspectRatio || null,
37042
37482
  showGrid,
37043
37483
  shade,
37044
37484
  initialCoverage: 0.5,
@@ -37060,21 +37500,24 @@ function CropperView({
37060
37500
  const toggleGrid = useCallback(() => setShowGrid((v) => !v), []);
37061
37501
  const toggleShade = useCallback(() => setShade((v) => !v), []);
37062
37502
  const toggleTips = useCallback(() => setShowTips((v) => !v), []);
37503
+ const toggleVisualOptions = useCallback(
37504
+ () => setShowVisualOptions((v) => !v),
37505
+ []
37506
+ );
37507
+ const toggleSelectorOptions = useCallback(
37508
+ () => setShowSelectorOptions((v) => !v),
37509
+ []
37510
+ );
37511
+ const toggleImageOptions = useCallback(
37512
+ () => setShowImageOptions((v) => !v),
37513
+ []
37514
+ );
37063
37515
  const centerImage = useCallback(() => transform.center(), [transform]);
37064
37516
  const centerSelection = useCallback(() => selection.center(), [selection]);
37065
37517
  const resetSelection = useCallback(() => selection.reset(), [selection]);
37066
37518
  const move = useCallback((x, y) => transform.move(x, y), [transform]);
37067
37519
  const zoom = useCallback((factor) => transform.zoom(factor), [transform]);
37068
37520
  const rotate = useCallback((deg) => transform.rotate(deg), [transform]);
37069
- const resetAll = useCallback(() => {
37070
- utils.resetAll();
37071
- setAspectRatio("");
37072
- setShowGrid(true);
37073
- setShade(true);
37074
- setFlipStates({ horizontal: false, vertical: false });
37075
- setShowPreview(false);
37076
- setPreviewUrl(null);
37077
- }, [utils]);
37078
37521
  const setSelectionCoverage = useCallback(
37079
37522
  (coverage) => {
37080
37523
  selection.setCoverage(coverage);
@@ -37096,16 +37539,188 @@ function CropperView({
37096
37539
  return { ...prev, vertical: newVertical };
37097
37540
  });
37098
37541
  }, [transform]);
37099
- const handleAspectRatio = useCallback(
37100
- (ratioValue) => {
37101
- setAspectRatio(ratioValue);
37102
- const ratioConfig = allowedAspectRatios.find(
37103
- (r) => r.value === ratioValue
37542
+ const saveCurrentCropState = useCallback(() => {
37543
+ if (!cropper.manager || !state.isReady) return;
37544
+ try {
37545
+ const currentCropData = cropData ? { ...cropData } : null;
37546
+ const currentZoom = cropper.manager.transform.getZoom();
37547
+ const currentRotation = cropper.manager.transform.getRotation();
37548
+ const savedState = {
37549
+ cropData: currentCropData,
37550
+ transforms: {
37551
+ zoom: currentZoom,
37552
+ rotation: currentRotation,
37553
+ flipHorizontal: flipStates.horizontal,
37554
+ flipVertical: flipStates.vertical
37555
+ }
37556
+ };
37557
+ setCrops(
37558
+ (prevCrops) => prevCrops.map(
37559
+ (crop, index) => index === activeCropIndex ? { ...crop, savedState } : crop
37560
+ )
37104
37561
  );
37105
- const numericRatio = ratioConfig ? ratioConfig.ratio : ratioValue;
37106
- selection.setAspectRatio(numericRatio);
37562
+ return savedState;
37563
+ } catch (error) {
37564
+ console.warn("Error saving crop state:", error);
37565
+ return null;
37566
+ }
37567
+ }, [cropper.manager, state.isReady, cropData, flipStates, activeCropIndex]);
37568
+ const restoreCropState = useCallback(
37569
+ (cropState) => {
37570
+ if (!cropper.manager || !state.isReady || !cropState) return;
37571
+ try {
37572
+ const { cropData: savedCropData, transforms } = cropState;
37573
+ if (transforms) {
37574
+ utils.resetAll();
37575
+ if (transforms.zoom && transforms.zoom !== 1) {
37576
+ cropper.manager.transform.setZoom(transforms.zoom);
37577
+ }
37578
+ if (transforms.rotation && transforms.rotation !== 0) {
37579
+ cropper.manager.transform.setRotation(transforms.rotation);
37580
+ }
37581
+ if (transforms.flipHorizontal) {
37582
+ cropper.manager.transform.flipHorizontal();
37583
+ }
37584
+ if (transforms.flipVertical) {
37585
+ cropper.manager.transform.flipVertical();
37586
+ }
37587
+ setFlipStates({
37588
+ horizontal: transforms.flipHorizontal || false,
37589
+ vertical: transforms.flipVertical || false
37590
+ });
37591
+ }
37592
+ if (savedCropData) {
37593
+ const { x, y, width, height } = savedCropData;
37594
+ selection.set(x, y, width, height);
37595
+ }
37596
+ } catch (error) {
37597
+ console.warn("Error restoring crop state:", error);
37598
+ }
37599
+ },
37600
+ [cropper.manager, state.isReady, selection, utils]
37601
+ );
37602
+ const validateCropNames = useCallback(() => {
37603
+ for (let i = 0; i < crops.length; i++) {
37604
+ const crop = crops[i];
37605
+ if (!crop.label || crop.label.trim() === "") {
37606
+ return i;
37607
+ }
37608
+ }
37609
+ return -1;
37610
+ }, [crops]);
37611
+ const switchToCrop = useCallback(
37612
+ (newIndex) => {
37613
+ if (newIndex === activeCropIndex) return;
37614
+ saveCurrentCropState();
37615
+ setActiveCropIndex(newIndex);
37616
+ setShouldCenter(true);
37617
+ },
37618
+ [activeCropIndex, saveCurrentCropState]
37619
+ );
37620
+ const addCustomCrop = useCallback(() => {
37621
+ if (!cropConfig.allowCustomCrops) {
37622
+ alert("No se pueden añadir recortes personalizados en este modo.");
37623
+ return;
37624
+ }
37625
+ saveCurrentCropState();
37626
+ const newCropId = `crop-custom-${Date.now()}`;
37627
+ const newCrop = {
37628
+ id: newCropId,
37629
+ label: `Recorte ${crops.length + 1}`,
37630
+ width: image.width || 1920,
37631
+ height: image.height || 1080,
37632
+ required: false,
37633
+ isCustom: true,
37634
+ confirmed: false,
37635
+ savedState: null
37636
+ };
37637
+ setCrops((prevCrops) => [...prevCrops, newCrop]);
37638
+ setActiveCropIndex(crops.length);
37639
+ accessibilityManager?.announce(
37640
+ `Nuevo recorte personalizado añadido: ${newCrop.label}`
37641
+ );
37642
+ }, [cropConfig.allowCustomCrops, saveCurrentCropState, crops.length, image.width, image.height, accessibilityManager]);
37643
+ const updateCropDimensions = useCallback(
37644
+ (field, value) => {
37645
+ const numValue = parseInt(value, 10);
37646
+ if (value === "" || isNaN(numValue)) {
37647
+ return;
37648
+ }
37649
+ setCrops(
37650
+ (prevCrops) => prevCrops.map(
37651
+ (crop, index) => index === activeCropIndex ? { ...crop, [field]: numValue } : crop
37652
+ )
37653
+ );
37654
+ },
37655
+ [activeCropIndex]
37656
+ );
37657
+ const validateAndApplyCropDimensions = useCallback(
37658
+ (field) => {
37659
+ const currentValue = activeCrop[field];
37660
+ let minValue = 100;
37661
+ if (canvasRef.current && imageInfo) {
37662
+ const canvasRect = canvasRef.current.getBoundingClientRect();
37663
+ const canvasMinSize = Math.min(canvasRect.width, canvasRect.height);
37664
+ minValue = Math.max(100, Math.round(canvasMinSize * 0.05));
37665
+ }
37666
+ const clampedValue = Math.max(minValue, Math.min(5e3, currentValue));
37667
+ if (clampedValue !== currentValue) {
37668
+ setCrops(
37669
+ (prevCrops) => prevCrops.map(
37670
+ (crop, index) => index === activeCropIndex ? { ...crop, [field]: clampedValue } : crop
37671
+ )
37672
+ );
37673
+ }
37674
+ const updatedCrop = { ...activeCrop, [field]: clampedValue };
37675
+ const newRatio = updatedCrop.width / updatedCrop.height;
37676
+ const currentSelection = selection.getData?.();
37677
+ selection.setAspectRatio(newRatio);
37678
+ if (currentSelection && currentSelection.x !== void 0) {
37679
+ setTimeout(() => {
37680
+ selection.set?.(
37681
+ currentSelection.x,
37682
+ currentSelection.y,
37683
+ currentSelection.width,
37684
+ currentSelection.height
37685
+ );
37686
+ }, 50);
37687
+ }
37688
+ },
37689
+ [activeCropIndex, activeCrop, selection, canvasRef, imageInfo]
37690
+ );
37691
+ const updateCropLabel = useCallback(
37692
+ (newLabel) => {
37693
+ setCrops(
37694
+ (prevCrops) => prevCrops.map(
37695
+ (crop, index) => index === activeCropIndex ? { ...crop, label: newLabel } : crop
37696
+ )
37697
+ );
37698
+ },
37699
+ [activeCropIndex]
37700
+ );
37701
+ const removeCustomCrop = useCallback(
37702
+ (cropIndex) => {
37703
+ const cropToRemove = crops[cropIndex];
37704
+ if (cropToRemove.required) {
37705
+ alert("No se puede eliminar un recorte obligatorio.");
37706
+ return;
37707
+ }
37708
+ if (crops.length === 1) {
37709
+ alert("Debe haber al menos un recorte.");
37710
+ return;
37711
+ }
37712
+ setCrops(
37713
+ (prevCrops) => prevCrops.filter((_, index) => index !== cropIndex)
37714
+ );
37715
+ if (cropIndex === activeCropIndex) {
37716
+ const newIndex = Math.max(0, cropIndex - 1);
37717
+ setActiveCropIndex(newIndex);
37718
+ } else if (cropIndex < activeCropIndex) {
37719
+ setActiveCropIndex((prev) => prev - 1);
37720
+ }
37721
+ accessibilityManager?.announce(`Recorte ${cropToRemove.label} eliminado`);
37107
37722
  },
37108
- [selection, allowedAspectRatios]
37723
+ [crops, activeCropIndex, accessibilityManager]
37109
37724
  );
37110
37725
  const generatePreview = useCallback(async () => {
37111
37726
  if (!canExport) return null;
@@ -37116,12 +37731,15 @@ function CropperView({
37116
37731
  imageSmoothingEnabled: true,
37117
37732
  imageSmoothingQuality: "high"
37118
37733
  });
37119
- return canvas ? canvas.toDataURL("image/jpeg", 0.9) : null;
37734
+ return canvas ? canvas.toDataURL(
37735
+ `image/${globalThis.downloadFormat || image.mime_type.split("/")[1] || "webp"}`,
37736
+ 0.9
37737
+ ) : null;
37120
37738
  } catch (error) {
37121
37739
  console.warn("Error generating preview:", error);
37122
37740
  return null;
37123
37741
  }
37124
- }, [canExport, selection]);
37742
+ }, [canExport, image.mime_type, selection]);
37125
37743
  const preview = useCallback(async () => {
37126
37744
  if (showPreview) {
37127
37745
  setShowPreview(false);
@@ -37151,7 +37769,7 @@ function CropperView({
37151
37769
  setPreviewLoading(false);
37152
37770
  }
37153
37771
  }, [canExport, generatePreview, showPreview]);
37154
- const saveCrop = useCallback(async () => {
37772
+ const performSaveCrop = useCallback(async () => {
37155
37773
  if (!canExport) {
37156
37774
  const errorMsg = "No se puede exportar el recorte por restricciones de CORS en la imagen original.";
37157
37775
  accessibilityManager?.announceError(errorMsg);
@@ -37164,7 +37782,7 @@ function CropperView({
37164
37782
  alert(errorMsg);
37165
37783
  return;
37166
37784
  }
37167
- accessibilityManager?.announce("Creando variante recortada de la imagen");
37785
+ accessibilityManager?.announce("Creando recorte de la imagen");
37168
37786
  try {
37169
37787
  if (!cropData || !effectiveImageInfo) {
37170
37788
  console.error("❌ Datos faltantes:", { cropData, effectiveImageInfo });
@@ -37192,60 +37810,181 @@ function CropperView({
37192
37810
  width: width / naturalWidth,
37193
37811
  height: height / naturalHeight
37194
37812
  };
37195
- const cropAspectRatio = width / height;
37196
- let variantWidth, variantHeight;
37197
- const maxDimension = 1200;
37198
- if (cropAspectRatio > 1) {
37199
- variantWidth = Math.min(width, maxDimension);
37200
- variantHeight = Math.round(variantWidth / cropAspectRatio);
37201
- } else {
37202
- variantHeight = Math.min(height, maxDimension);
37203
- variantWidth = Math.round(variantHeight * cropAspectRatio);
37204
- }
37813
+ const variantWidth = Math.min(activeCrop.width, 5e3);
37814
+ const variantHeight = Math.min(activeCrop.height, 5e3);
37205
37815
  const ts = Date.now();
37206
- const [name] = image.filename.split(".");
37207
- const variantName = `${name}_crop_${ts}`;
37816
+ const variantName = `${editableFilename}_${activeCrop.label || "crop"}_${ts}`;
37208
37817
  const result = await createCropVariant(image.id, cropParams, {
37209
37818
  name: variantName,
37210
37819
  width: variantWidth,
37211
37820
  height: variantHeight,
37212
- format: "webp",
37821
+ format: globalThis.downloadFormat || "webp",
37213
37822
  quality: 90
37214
37823
  });
37215
37824
  if (result) {
37216
- accessibilityManager?.announceSuccess(
37217
- `Variante recortada creada: ${variantName}`
37218
- );
37825
+ accessibilityManager?.announceSuccess(`Recorte creado: ${variantName}`);
37219
37826
  onVariantCreated?.(image.id, result);
37220
37827
  onSave(result);
37221
37828
  }
37222
37829
  } catch (error) {
37223
37830
  console.warn("Error creating crop variant:", error);
37224
- const errorMsg = "No se pudo crear la variante recortada. Inténtalo de nuevo.";
37831
+ const errorMsg = "No se pudo crear el recorte. Inténtalo de nuevo.";
37225
37832
  accessibilityManager?.announceError(errorMsg);
37226
37833
  alert(errorMsg);
37834
+ onError?.(error);
37227
37835
  }
37228
37836
  }, [
37229
37837
  canExport,
37838
+ state.isReady,
37839
+ accessibilityManager,
37230
37840
  cropData,
37231
37841
  effectiveImageInfo,
37232
- image.filename,
37233
- image.id,
37842
+ editableFilename,
37843
+ activeCrop.label,
37844
+ activeCrop.width,
37845
+ activeCrop.height,
37234
37846
  createCropVariant,
37235
- onSave,
37847
+ image.id,
37236
37848
  onVariantCreated,
37849
+ onSave,
37850
+ onError
37851
+ ]);
37852
+ const saveCrop = useCallback(async () => {
37853
+ const invalidCropIndex = validateCropNames();
37854
+ if (invalidCropIndex !== -1) {
37855
+ const cropWithoutName = crops[invalidCropIndex];
37856
+ alert(`El recorte "${cropWithoutName.label || cropWithoutName || "(sin nombre)"}" debe tener un nombre válido.`);
37857
+ setActiveCropIndex(invalidCropIndex);
37858
+ return;
37859
+ }
37860
+ if (crops.length > 1) {
37861
+ setPendingAction("save");
37862
+ setSelectedCropsForAction(crops.map((_, index) => index));
37863
+ setShowConfirmModal(true);
37864
+ return;
37865
+ }
37866
+ await performSaveCrop();
37867
+ }, [validateCropNames, crops, performSaveCrop]);
37868
+ const performDownload = useCallback(async () => {
37869
+ if (!canExport) {
37870
+ const errorMsg = "No se puede descargar el recorte por restricciones de CORS en la imagen original.";
37871
+ accessibilityManager?.announceError(errorMsg);
37872
+ alert(errorMsg);
37873
+ return;
37874
+ }
37875
+ try {
37876
+ accessibilityManager?.announce("Preparando descarga del recorte");
37877
+ let downloadUrl = previewUrl;
37878
+ if (!downloadUrl) {
37879
+ downloadUrl = await generatePreview();
37880
+ }
37881
+ if (!downloadUrl) {
37882
+ throw new Error("No se pudo generar la imagen para descargar");
37883
+ }
37884
+ const cropName = activeCrop.label.replace(/\.[^/.]+$/, " ").replace(" ", "-").trim();
37885
+ const filename = `${editableFilename}_${cropName || "crop"}`;
37886
+ await downloadImage(downloadUrl, filename, {
37887
+ accessibilityManager,
37888
+ onSuccess: (finalFilename) => {
37889
+ accessibilityManager?.announce(
37890
+ `Recorte descargado como ${finalFilename}`
37891
+ );
37892
+ },
37893
+ onError: (error) => {
37894
+ accessibilityManager?.announceError(
37895
+ `Error al descargar: ${error.message}`
37896
+ );
37897
+ alert(`Error al descargar la imagen: ${error.message}`);
37898
+ }
37899
+ });
37900
+ } catch (error) {
37901
+ console.error("Error downloading crop:", error);
37902
+ accessibilityManager?.announceError(
37903
+ `Error al descargar el recorte: ${error.message}`
37904
+ );
37905
+ alert(`Error al descargar el recorte: ${error.message}`);
37906
+ }
37907
+ }, [
37908
+ canExport,
37237
37909
  accessibilityManager,
37238
- state.isReady
37910
+ previewUrl,
37911
+ activeCrop.label,
37912
+ editableFilename,
37913
+ generatePreview
37239
37914
  ]);
37915
+ const handleDownload = useCallback(async () => {
37916
+ const invalidCropIndex = validateCropNames();
37917
+ if (invalidCropIndex !== -1) {
37918
+ const cropWithoutName = crops[invalidCropIndex];
37919
+ alert(`El recorte "${cropWithoutName.label || cropWithoutName || "(sin nombre)"}" debe tener un nombre válido.`);
37920
+ setActiveCropIndex(invalidCropIndex);
37921
+ return;
37922
+ }
37923
+ if (crops.length > 1) {
37924
+ setPendingAction("download");
37925
+ setSelectedCropsForAction(crops.map((_, index) => index));
37926
+ setShowConfirmModal(true);
37927
+ return;
37928
+ }
37929
+ await performDownload();
37930
+ }, [validateCropNames, crops, performDownload]);
37240
37931
  useEffect(() => {
37241
37932
  setShowPreview(false);
37242
37933
  setPreviewUrl(null);
37243
37934
  }, [image]);
37244
37935
  useEffect(() => {
37245
37936
  if (!cropper.manager) return;
37246
- selection.setAspectRatio(aspectRatio);
37937
+ if (calculatedAspectRatio) {
37938
+ selection.setAspectRatio(calculatedAspectRatio);
37939
+ }
37247
37940
  utils.setBackground(shade);
37248
- }, [aspectRatio, shade, cropper.manager, selection, utils]);
37941
+ }, [calculatedAspectRatio, shade, cropper.manager, selection, utils]);
37942
+ useEffect(() => {
37943
+ if (!cropper.manager || !state.isReady) return;
37944
+ const cropState = activeCrop?.savedState;
37945
+ if (cropState) {
37946
+ restoreCropState(cropState);
37947
+ setShouldCenter(false);
37948
+ } else {
37949
+ if (calculatedAspectRatio) {
37950
+ selection.setAspectRatio(calculatedAspectRatio);
37951
+ if (shouldCenter) {
37952
+ setTimeout(() => {
37953
+ selection.center();
37954
+ setShouldCenter(false);
37955
+ }, 100);
37956
+ }
37957
+ }
37958
+ }
37959
+ }, [
37960
+ activeCropIndex,
37961
+ activeCrop,
37962
+ calculatedAspectRatio,
37963
+ cropper.manager,
37964
+ state.isReady,
37965
+ restoreCropState,
37966
+ selection,
37967
+ shouldCenter
37968
+ ]);
37969
+ useEffect(() => {
37970
+ if (!imageInfo || !state.isReady || cropConfig.mandatoryCrops.length > 0)
37971
+ return;
37972
+ const defaultCrop = crops[0];
37973
+ if (defaultCrop && defaultCrop.id === "crop-default-0" && (defaultCrop.width === 1920 || defaultCrop.height === 1080)) {
37974
+ const newWidth = Math.min(imageInfo.naturalWidth, 5e3);
37975
+ const newHeight = Math.min(imageInfo.naturalHeight, 5e3);
37976
+ setCrops([
37977
+ {
37978
+ ...defaultCrop,
37979
+ width: newWidth,
37980
+ height: newHeight
37981
+ }
37982
+ ]);
37983
+ console.log(
37984
+ `[CropperView] Crop por defecto actualizado a ${newWidth}×${newHeight}px`
37985
+ );
37986
+ }
37987
+ }, [imageInfo, state.isReady, cropConfig.mandatoryCrops.length, crops]);
37249
37988
  useEffect(() => {
37250
37989
  if (!cropper.manager || !cropper.manager.utils) return;
37251
37990
  try {
@@ -37289,7 +38028,7 @@ function CropperView({
37289
38028
  showPreview,
37290
38029
  canExport,
37291
38030
  generatePreview,
37292
- aspectRatio,
38031
+ calculatedAspectRatio,
37293
38032
  flipStates,
37294
38033
  previewUrl
37295
38034
  ]);
@@ -37322,19 +38061,27 @@ function CropperView({
37322
38061
  };
37323
38062
  }, [canvasRef]);
37324
38063
  if (!image) return null;
37325
- return /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-view px-2 border-2 border-gray-200/50 rounded-lg min-w-fit max-w-7xl mx-auto h-screen flex flex-col", children: [
37326
- /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-header flex flex-col sm:flex-row justify-between items-start sm:items-center p-4 sm:p-6 pb-4 border-b border-gray-200 bg-white z-10 flex-shrink-0 gap-4 sm:gap-2", children: [
37327
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
37328
- /* @__PURE__ */ jsx("h2", { className: "text-lg sm:text-xl font-bold text-gray-800 truncate", children: "Recortar imagen" }),
37329
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 mt-1 truncate", children: image.filename })
38064
+ return /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-view px-2 border-2 border-gray-200/50 rounded-lg min-w-fit max-w-7xl mx-auto h-full min-h-full flex flex-col", children: [
38065
+ /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-header flex flex-col sm:flex-row justify-between items-start sm:items-center p-4 lg:p-6 pb-4 border-b border-gray-200 bg-white z-10 flex-shrink-0 gap-4 lg:gap-2", children: [
38066
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 space-y-2", children: [
38067
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38068
+ /* @__PURE__ */ jsx("h2", { className: "text-lg sm:text-xl font-bold text-gray-800", children: "Generar recortes" }),
38069
+ crops.length > 1 && /* @__PURE__ */ jsx("span", { className: "px-2 py-0.5 bg-blue-100 text-blue-800 text-xs font-semibold rounded", children: activeCrop.label })
38070
+ ] }),
38071
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38072
+ /* @__PURE__ */ jsx("label", { className: "text-xs text-gray-500 whitespace-nowrap", children: "Nombre:" }),
38073
+ /* @__PURE__ */ jsx("div", { className: "", children: editableFilename + "." + (globalThis.downloadFormat || image.mime_type.split("/")[1] || "webp") })
38074
+ ] })
37330
38075
  ] }),
37331
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2 w-full sm:w-auto", children: [
38076
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-row self-end gap-2 w-full sm:w-auto", children: [
37332
38077
  /* @__PURE__ */ jsx(
37333
38078
  "button",
37334
38079
  {
37335
38080
  onClick: onCancel,
37336
38081
  disabled: creatingVariant,
37337
- className: "limbo-btn limbo-btn-secondary px-4 py-2 flex-1 sm:flex-initial",
38082
+ className: "limbo-btn limbo-btn-secondary px-4 sm:py-1 h-min flex-1",
38083
+ "aria-label": "Cancelar y volver",
38084
+ title: "Cancelar y volver",
37338
38085
  children: "Cancelar"
37339
38086
  }
37340
38087
  ),
@@ -37343,8 +38090,9 @@ function CropperView({
37343
38090
  {
37344
38091
  onClick: () => onDelete?.(image),
37345
38092
  disabled: deleting | creatingVariant,
37346
- className: "limbo-btn limbo-btn-danger px-4 py-2 flex-1 sm:flex-initial",
38093
+ className: "limbo-btn limbo-btn-danger px-4 sm:py-1 h-min flex-1 sm:flex-initial",
37347
38094
  "aria-label": `Eliminar imagen ${image.filename}`,
38095
+ title: `Eliminar imagen ${image.filename}`,
37348
38096
  children: deleting ? "Eliminando..." : "Eliminar"
37349
38097
  }
37350
38098
  )
@@ -37356,15 +38104,6 @@ function CropperView({
37356
38104
  " ",
37357
38105
  variantError
37358
38106
  ] }),
37359
- cropData && cropData.width > 0 && /* @__PURE__ */ jsxs("div", { className: "alert alert-info mb-2 text-sm", role: "status", children: [
37360
- /* @__PURE__ */ jsx("strong", { children: "Área:" }),
37361
- " ",
37362
- Math.round(cropData.width),
37363
- " ×",
37364
- " ",
37365
- Math.round(cropData.height),
37366
- " px"
37367
- ] }),
37368
38107
  imageInfo && /* @__PURE__ */ jsxs("div", { className: "alert alert-secondary mb-2 text-sm", role: "status", children: [
37369
38108
  /* @__PURE__ */ jsx("strong", { children: "Original:" }),
37370
38109
  " ",
@@ -37382,10 +38121,135 @@ function CropperView({
37382
38121
  " px)"
37383
38122
  ] })
37384
38123
  ] }),
37385
- !canExport && /* @__PURE__ */ jsx("div", { className: "alert alert-warning mb-2 text-sm", role: "alert", children: "⚠️ No se puede exportar por restricciones CORS" })
38124
+ !canExport && /* @__PURE__ */ jsx("div", { className: "alert alert-warning mb-2 text-sm", role: "alert", children: "⚠️ No se puede exportar por restricciones CORS" }),
38125
+ /* @__PURE__ */ jsxs("div", { className: "bg-white border-b border-gray-200 p-3 sm:p-4 flex-shrink-0 sticky top-0 z-10 mb-3 shadow-sm", children: [
38126
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
38127
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold text-gray-700", children: [
38128
+ "Recortes ",
38129
+ crops.length > 1 && `(${crops.length})`
38130
+ ] }),
38131
+ cropConfig.allowCustomCrops && /* @__PURE__ */ jsxs(
38132
+ "button",
38133
+ {
38134
+ onClick: addCustomCrop,
38135
+ disabled: creatingVariant,
38136
+ className: "text-xs cursor-pointer px-2 py-1 bg-green-100 hover:bg-green-200 text-green-800 rounded border border-green-300 transition-colors flex items-center gap-1",
38137
+ title: "Añadir recorte personalizado",
38138
+ children: [
38139
+ /* @__PURE__ */ jsx("span", { className: "icon icon-add-green text-green-800 icon--xs" }),
38140
+ "Añadir recorte"
38141
+ ]
38142
+ }
38143
+ )
38144
+ ] }),
38145
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 max-h-48 overflow-y-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 justify-items-stretch items-start justify-stretch w-full", children: crops.map((crop, index) => /* @__PURE__ */ jsxs(
38146
+ "label",
38147
+ {
38148
+ className: `flex items-center gap-3 p-2 rounded border cursor-pointer transition-colors ${activeCropIndex === index ? "bg-blue-50 border-blue-300" : "bg-gray-50 border-gray-200 hover:bg-gray-100"}`,
38149
+ children: [
38150
+ /* @__PURE__ */ jsx(
38151
+ "input",
38152
+ {
38153
+ type: "radio",
38154
+ name: "active-crop",
38155
+ checked: activeCropIndex === index,
38156
+ onChange: () => switchToCrop(index),
38157
+ disabled: creatingVariant,
38158
+ className: "w-4 h-4 text-blue-600 hidden"
38159
+ }
38160
+ ),
38161
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
38162
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38163
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-800 truncate", children: activeCrop.label === crop.label && crop.required === false ? /* @__PURE__ */ jsx(
38164
+ "input",
38165
+ {
38166
+ type: "text",
38167
+ value: crop.label,
38168
+ onChange: (e) => updateCropLabel(e.target.value),
38169
+ disabled: creatingVariant || crop.required,
38170
+ className: "w-full text-sm px-2 py-1.5 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed",
38171
+ placeholder: "Nombre del recorte",
38172
+ required: true,
38173
+ maxLength: 25
38174
+ }
38175
+ ) : crop.label }),
38176
+ crop.required && /* @__PURE__ */ jsx("span", { className: "text-xs px-1.5 py-0.5 bg-red-100 text-red-700 rounded", children: "Obligatorio" }),
38177
+ crop.isCustom && /* @__PURE__ */ jsx("span", { className: "text-xs px-1.5 py-0.5 bg-purple-100 text-purple-700 rounded", children: "Personalizado" })
38178
+ ] }),
38179
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
38180
+ crop.width,
38181
+ " × ",
38182
+ crop.height,
38183
+ " px"
38184
+ ] })
38185
+ ] }),
38186
+ !crop.required && crops.length > 1 && /* @__PURE__ */ jsx(
38187
+ "button",
38188
+ {
38189
+ onClick: (e) => {
38190
+ e.preventDefault();
38191
+ removeCustomCrop(index);
38192
+ },
38193
+ className: `flex items-stretch group max-h-fit hover:bg-red-500/50 text-red-500 hover:text-red-700 p-1 rounded-full aspect-square cursor-pointer`,
38194
+ title: "Eliminar recorte",
38195
+ children: /* @__PURE__ */ jsx(
38196
+ "span",
38197
+ {
38198
+ className: `group-hover:bg-gray-transparent-50 icon icon-close-small icon--xs`
38199
+ }
38200
+ )
38201
+ }
38202
+ )
38203
+ ]
38204
+ },
38205
+ crop.id
38206
+ )) }),
38207
+ activeCrop && /* @__PURE__ */ jsx("div", { className: "mt-4 pt-4 border-t border-gray-200 space-y-3", children: /* @__PURE__ */ jsxs("div", { children: [
38208
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-700 block mb-1", children: "Dimensiones (px)" }),
38209
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38210
+ /* @__PURE__ */ jsx(
38211
+ "input",
38212
+ {
38213
+ type: "number",
38214
+ min: "100",
38215
+ max: "5000",
38216
+ value: activeCrop.width,
38217
+ onChange: (e) => updateCropDimensions("width", e.target.value),
38218
+ onBlur: () => validateAndApplyCropDimensions("width"),
38219
+ disabled: creatingVariant,
38220
+ className: "flex-1 text-sm px-2 py-1.5 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
38221
+ placeholder: "Ancho"
38222
+ }
38223
+ ),
38224
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400 font-bold", children: "×" }),
38225
+ /* @__PURE__ */ jsx(
38226
+ "input",
38227
+ {
38228
+ type: "number",
38229
+ min: "100",
38230
+ max: "5000",
38231
+ value: activeCrop.height,
38232
+ onChange: (e) => updateCropDimensions("height", e.target.value),
38233
+ onBlur: () => validateAndApplyCropDimensions("height"),
38234
+ disabled: creatingVariant,
38235
+ className: "flex-1 text-sm px-2 py-1.5 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
38236
+ placeholder: "Alto"
38237
+ }
38238
+ )
38239
+ ] }),
38240
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mt-1", children: [
38241
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
38242
+ "Proporción:",
38243
+ " ",
38244
+ calculatedAspectRatio ? calculatedAspectRatio.toFixed(2) : "N/A"
38245
+ ] }),
38246
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: "Min: 100px | Max: 5000px" })
38247
+ ] })
38248
+ ] }) })
38249
+ ] })
37386
38250
  ] }),
37387
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col md:flex-row flex-1 overflow-hidden rounded-lg gap-4 md:gap-0 cropper-main-content-area", children: [
37388
- /* @__PURE__ */ jsx("div", { className: "flex-1 relative min-w-0 order-2 md:order-1", children: /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-container absolute inset-2 sm:inset-4 bg-white rounded-lg border-2 border-gray-200 shadow-lg overflow-hidden", children: [
38251
+ /* @__PURE__ */ jsxs("div", { className: "cropper-main-content-area flex flex-col lg:flex-row flex-1 overflow-hidden rounded-lg gap-4 md:gap-0 aspect-auto lg:aspect-square xl:aspect-auto w-full max-w-[95%] lg:max-w-full self-center lg:max-h-[50rem]", children: [
38252
+ /* @__PURE__ */ jsx("div", { className: "flex-1 relative min-w-0 order-1", children: /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-container lg:absolute inset-2 sm:inset-4 bg-white rounded-lg border-2 border-gray-200 shadow-lg overflow-hidden", children: [
37389
38253
  showPreview && /* @__PURE__ */ jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 bg-white rounded-lg border border-gray-300 p-2 sm:p-3 z-50 shadow-xl w-48 sm:w-64 max-w-[calc(50%-1rem)] sm:max-w-[calc(50%-2rem)] md:min-w-1/4", children: [
37390
38254
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-2", children: [
37391
38255
  /* @__PURE__ */ jsx("h3", { className: "text-xs font-semibold text-gray-700", children: "Vista previa" }),
@@ -37458,9 +38322,13 @@ function CropperView({
37458
38322
  "initial-coverage": "0.5",
37459
38323
  movable: true,
37460
38324
  resizable: true,
37461
- keyboard: "false",
38325
+ keyboard: "true",
37462
38326
  action: "move",
37463
- style: { cursor: "move" },
38327
+ style: {
38328
+ cursor: "move",
38329
+ minWidth: `${minCropSizes.minWidth}px`,
38330
+ minHeight: `${minCropSizes.minHeight}px`
38331
+ },
37464
38332
  "data-prevent-delete": "true",
37465
38333
  tabindex: "-1",
37466
38334
  children: [
@@ -37503,61 +38371,401 @@ function CropperView({
37503
38371
  }
37504
38372
  )
37505
38373
  ] }) }),
37506
- /* @__PURE__ */ jsx("div", { className: "limbo-cropper-controls w-full md:w-80 bg-white border-t md:border-t-0 md:border-l border-gray-200 overflow-y-auto flex-shrink-0 order-1 md:order-2 max-h-96 md:max-h-none", children: /* @__PURE__ */ jsxs("div", { className: "p-3 sm:p-4 space-y-3 sm:space-y-4 relative", children: [
37507
- /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4 md:sticky top-0 z-50", children: [
37508
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Acciones" }),
37509
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
38374
+ /* @__PURE__ */ jsx("div", { className: "flex limbo-cropper-controls w-full lg:w-80 h-full bg-white border-t md:border-t-0 md:border-l border-gray-200 lg:overflow-y-auto flex-shrink-0 order-1 max-h-none", children: /* @__PURE__ */ jsxs("div", { className: "p-3 min-h-full sm:p-4 space-y-3 sm:space-y-4 relative w-full grid grid-cols-1 lg:flex lg:flex-col", children: [
38375
+ /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4", children: [
38376
+ /* @__PURE__ */ jsx(
38377
+ "button",
38378
+ {
38379
+ onClick: toggleVisualOptions,
38380
+ className: "w-full p-2 my-1 cursor-pointer hover:bg-neutral-gray-050/50 transition-colors rounded-md",
38381
+ "aria-expanded": showVisualOptions,
38382
+ "aria-label": "Mostrar/ocultar opciones de visualizción",
38383
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
38384
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Visualización" }),
38385
+ /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(
38386
+ "span",
38387
+ {
38388
+ className: `icon ${!showVisualOptions ? "icon-chevron-down" : "icon-chevron-up"} text-center icon--xs`
38389
+ }
38390
+ ) })
38391
+ ] })
38392
+ }
38393
+ ),
38394
+ showVisualOptions && /* @__PURE__ */ jsxs("div", { className: "pb-2 space-y-2", children: [
37510
38395
  /* @__PURE__ */ jsxs(
37511
38396
  "button",
37512
38397
  {
37513
- onClick: preview,
37514
- disabled: creatingVariant || !canExport,
37515
- className: `w-full min-h-10 transition-colors ${showPreview ? "limbo-btn limbo-btn-danger" : "limbo-btn limbo-btn-primary"}`,
37516
- "aria-label": "Generar vista previa del recorte",
38398
+ onClick: toggleGrid,
38399
+ className: `w-full flex cursor-pointer items-center justify-between p-2 rounded transition-colors ${showGrid ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
38400
+ disabled: creatingVariant,
38401
+ "aria-pressed": showGrid,
38402
+ title: "Mostrar/ocultar cuadrícula de la regla de los tercios en el selector",
38403
+ "aria-label": "Activar/desactivar grid",
37517
38404
  children: [
37518
- /* @__PURE__ */ jsx(
37519
- "span",
37520
- {
37521
- className: `icon ${showPreview ? "icon-close-small-white" : "icon-search-white"}`
37522
- }
37523
- ),
37524
- showPreview ? "Cerrar previa" : "Vista previa"
38405
+ /* @__PURE__ */ jsxs("span", { className: "text-sm flex items-center", children: [
38406
+ /* @__PURE__ */ jsx("span", { className: "icon icon-area-blue me-1" }),
38407
+ " ",
38408
+ "Cuadrícula"
38409
+ ] }),
38410
+ /* @__PURE__ */ jsx("span", { className: "text-xs", children: showGrid ? /* @__PURE__ */ jsxs(Fragment, { children: [
38411
+ /* @__PURE__ */ jsx("span", { className: "icon icon-tick icon--xs align-[middle!important] -mt-0.5" }),
38412
+ " ",
38413
+ "Activo"
38414
+ ] }) : "Inactivo" })
37525
38415
  ]
37526
38416
  }
37527
38417
  ),
37528
- /* @__PURE__ */ jsx(
37529
- "button",
37530
- {
37531
- onClick: saveCrop,
37532
- disabled: creatingVariant || !cropData || !effectiveImageInfo || !canExport || !state.isReady,
37533
- className: "w-full limbo-btn limbo-btn-success min-h-10",
37534
- "aria-label": "Guardar imagen recortada",
37535
- children: creatingVariant ? /* @__PURE__ */ jsxs(Fragment, { children: [
37536
- /* @__PURE__ */ jsx("span", { className: "icon icon-save-white" }),
37537
- " ",
37538
- "Guardando..."
37539
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
37540
- /* @__PURE__ */ jsx("span", { className: "icon icon-save-white" }),
37541
- " Guardar recorte"
37542
- ] })
37543
- }
37544
- ),
37545
38418
  /* @__PURE__ */ jsxs(
37546
38419
  "button",
37547
38420
  {
37548
- onClick: resetAll,
38421
+ onClick: toggleShade,
38422
+ className: `w-full flex cursor-pointer items-center justify-between p-2 rounded transition-colors ${shade ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
37549
38423
  disabled: creatingVariant,
37550
- className: "w-full limbo-btn limbo-btn-secondary min-h-10",
37551
- "aria-label": "Reiniciar todas las configuraciones",
38424
+ "aria-pressed": shade,
38425
+ "aria-label": "Activar/desactivar sombreado",
37552
38426
  children: [
37553
- /* @__PURE__ */ jsx("span", { className: "icon icon-refresh-white" }),
37554
- " Reiniciar todo"
38427
+ /* @__PURE__ */ jsxs("span", { className: "text-sm flex items-center", children: [
38428
+ /* @__PURE__ */ jsx("span", { className: "icon icon-comparison-blue me-1" }),
38429
+ " ",
38430
+ "Tablero"
38431
+ ] }),
38432
+ /* @__PURE__ */ jsx("span", { className: "text-xs", children: shade ? /* @__PURE__ */ jsxs(Fragment, { children: [
38433
+ /* @__PURE__ */ jsx("span", { className: "icon icon-tick icon--xs align-[middle!important] -mt-0.5" }),
38434
+ " ",
38435
+ "Activo"
38436
+ ] }) : "Inactivo" })
37555
38437
  ]
37556
38438
  }
37557
38439
  )
37558
38440
  ] })
37559
38441
  ] }),
37560
- /* @__PURE__ */ jsxs("div", { className: "bg-gradient-to-br from-brand-blue-50 to-green-50 rounded-lg border border-blue-200 shadow-sm", children: [
38442
+ /* @__PURE__ */ jsxs(
38443
+ "div",
38444
+ {
38445
+ className: "bg-white rounded-lg border border-gray-200 p-4" + (showSelectorOptions ? "" : " bg-neutral-gray-50/50"),
38446
+ children: [
38447
+ /* @__PURE__ */ jsx(
38448
+ "button",
38449
+ {
38450
+ onClick: toggleSelectorOptions,
38451
+ className: "w-full p-2 my-1 cursor-pointer hover:bg-neutral-gray-050/50 transition-colors rounded-md",
38452
+ "aria-expanded": showSelectorOptions,
38453
+ "aria-label": "Mostrar/ocultar opciones de visualizción",
38454
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
38455
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Ajustes del selector" }),
38456
+ /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(
38457
+ "span",
38458
+ {
38459
+ className: `icon ${!showSelectorOptions ? "icon-chevron-down" : "icon-chevron-up"} text-center icon--xs`
38460
+ }
38461
+ ) })
38462
+ ] })
38463
+ }
38464
+ ),
38465
+ showSelectorOptions && /* @__PURE__ */ jsxs("div", { children: [
38466
+ /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
38467
+ /* @__PURE__ */ jsx(
38468
+ "button",
38469
+ {
38470
+ onClick: () => setSelectionCoverage(0.5),
38471
+ className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38472
+ disabled: creatingVariant,
38473
+ "aria-label": "Selección 50%",
38474
+ title: "Tamaño de selector 50%",
38475
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "50%" }) })
38476
+ }
38477
+ ),
38478
+ /* @__PURE__ */ jsx(
38479
+ "button",
38480
+ {
38481
+ onClick: () => setSelectionCoverage(0.7),
38482
+ className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38483
+ disabled: creatingVariant,
38484
+ "aria-label": "Selección 70%",
38485
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "70%" }) })
38486
+ }
38487
+ ),
38488
+ /* @__PURE__ */ jsx(
38489
+ "button",
38490
+ {
38491
+ onClick: () => setSelectionCoverage(0.9),
38492
+ className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38493
+ disabled: creatingVariant,
38494
+ "aria-label": "Selección 90%",
38495
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "90%" }) })
38496
+ }
38497
+ ),
38498
+ /* @__PURE__ */ jsx(
38499
+ "button",
38500
+ {
38501
+ onClick: () => setSelectionCoverage(1),
38502
+ className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38503
+ disabled: creatingVariant,
38504
+ "aria-label": "Selección completa",
38505
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "100%" }) })
38506
+ }
38507
+ )
38508
+ ] }) }),
38509
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 pt-2", children: [
38510
+ /* @__PURE__ */ jsx(
38511
+ "button",
38512
+ {
38513
+ onClick: centerSelection,
38514
+ className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38515
+ title: "Centrar selección",
38516
+ disabled: creatingVariant,
38517
+ "aria-label": "Centrar área de selección",
38518
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
38519
+ /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button icon--sm align-[middle!important] " }),
38520
+ " ",
38521
+ "Centrar selección"
38522
+ ] }) })
38523
+ }
38524
+ ),
38525
+ /* @__PURE__ */ jsx(
38526
+ "button",
38527
+ {
38528
+ onClick: resetSelection,
38529
+ className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38530
+ title: "Reiniciar selección",
38531
+ disabled: creatingVariant,
38532
+ "aria-label": "Reiniciar área de selección",
38533
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
38534
+ /* @__PURE__ */ jsx("span", { className: "icon icon-refresh icon--sm lign-[middle!important] " }),
38535
+ " ",
38536
+ "Reset selección"
38537
+ ] }) })
38538
+ }
38539
+ )
38540
+ ] })
38541
+ ] })
38542
+ ]
38543
+ }
38544
+ ),
38545
+ /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4", children: [
38546
+ /* @__PURE__ */ jsx(
38547
+ "button",
38548
+ {
38549
+ onClick: toggleImageOptions,
38550
+ className: "w-full p-2 my-1 cursor-pointer hover:bg-neutral-gray-050/50 transition-colors rounded-md",
38551
+ "aria-expanded": showImageOptions,
38552
+ "aria-label": "Mostrar/ocultar opciones de visualizción",
38553
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
38554
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Transformar imagen" }),
38555
+ /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(
38556
+ "span",
38557
+ {
38558
+ className: `icon ${!showImageOptions ? "icon-chevron-down" : "icon-chevron-up"} text-center icon--xs`
38559
+ }
38560
+ ) })
38561
+ ] })
38562
+ }
38563
+ ),
38564
+ showImageOptions && /* @__PURE__ */ jsxs(Fragment, { children: [
38565
+ /* @__PURE__ */ jsxs("div", { className: "mb-4 hidden lg:block", children: [
38566
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Mover imagen" }),
38567
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-1", children: [
38568
+ /* @__PURE__ */ jsx("div", {}),
38569
+ /* @__PURE__ */ jsx(
38570
+ "button",
38571
+ {
38572
+ onClick: () => move(0, -10),
38573
+ className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38574
+ title: "Mover arriba",
38575
+ disabled: creatingVariant,
38576
+ "aria-label": "Mover imagen hacia arriba",
38577
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-up-blue" }) })
38578
+ }
38579
+ ),
38580
+ /* @__PURE__ */ jsx("div", {}),
38581
+ /* @__PURE__ */ jsx(
38582
+ "button",
38583
+ {
38584
+ onClick: () => move(-10, 0),
38585
+ className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38586
+ title: "Mover izquierda",
38587
+ disabled: creatingVariant,
38588
+ "aria-label": "Mover imagen hacia la izquierda",
38589
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-blue" }) })
38590
+ }
38591
+ ),
38592
+ /* @__PURE__ */ jsx(
38593
+ "button",
38594
+ {
38595
+ onClick: centerImage,
38596
+ className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38597
+ title: "Centrar y ajustar imagen",
38598
+ disabled: creatingVariant,
38599
+ "aria-label": "Centrar y ajustar imagen",
38600
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button-blue icon-md" }) })
38601
+ }
38602
+ ),
38603
+ /* @__PURE__ */ jsx(
38604
+ "button",
38605
+ {
38606
+ onClick: () => move(10, 0),
38607
+ className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38608
+ title: "Mover derecha",
38609
+ disabled: creatingVariant,
38610
+ "aria-label": "Mover imagen hacia la derecha",
38611
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-blue" }) })
38612
+ }
38613
+ ),
38614
+ /* @__PURE__ */ jsx("div", {}),
38615
+ /* @__PURE__ */ jsx(
38616
+ "button",
38617
+ {
38618
+ onClick: () => move(0, 10),
38619
+ className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38620
+ title: "Mover abajo",
38621
+ disabled: creatingVariant,
38622
+ "aria-label": "Mover imagen hacia abajo",
38623
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-down-blue" }) })
38624
+ }
38625
+ ),
38626
+ /* @__PURE__ */ jsx("div", {})
38627
+ ] })
38628
+ ] }),
38629
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
38630
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600 mb-2", children: [
38631
+ "Zoom ",
38632
+ zoomInfo ? zoomInfo.percentage + "%" : ""
38633
+ ] }),
38634
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
38635
+ /* @__PURE__ */ jsx(
38636
+ "button",
38637
+ {
38638
+ onClick: () => zoom(-0.2),
38639
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38640
+ title: "Alejar 20%",
38641
+ disabled: creatingVariant,
38642
+ "aria-label": "Alejar imagen",
38643
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-out-blue" }) })
38644
+ }
38645
+ ),
38646
+ /* @__PURE__ */ jsx(
38647
+ "button",
38648
+ {
38649
+ onClick: resetZoomOnly,
38650
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38651
+ title: "Restablecer zoom original",
38652
+ disabled: creatingVariant,
38653
+ "aria-label": "Restablecer el zoom para que la imagen se vea con su resolución original",
38654
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-screenshot-blue" }) })
38655
+ }
38656
+ ),
38657
+ /* @__PURE__ */ jsx(
38658
+ "button",
38659
+ {
38660
+ onClick: () => zoom(0.2),
38661
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38662
+ title: "Acercar 20%",
38663
+ disabled: creatingVariant,
38664
+ "aria-label": "Acercar imagen",
38665
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-in-blue" }) })
38666
+ }
38667
+ )
38668
+ ] })
38669
+ ] }),
38670
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
38671
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Rotación" }),
38672
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1 *:text-brand-blue-1000 *:text-sm", children: [
38673
+ /* @__PURE__ */ jsx(
38674
+ "button",
38675
+ {
38676
+ onClick: () => rotate(-90),
38677
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38678
+ title: "Rotar -90°",
38679
+ disabled: creatingVariant,
38680
+ "aria-label": "Rotar imagen 90 grados a la izquierda",
38681
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-reply-blue" }) })
38682
+ }
38683
+ ),
38684
+ /* @__PURE__ */ jsx(
38685
+ "button",
38686
+ {
38687
+ onClick: () => rotate(-45),
38688
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38689
+ title: "Rotar -45°",
38690
+ disabled: creatingVariant,
38691
+ "aria-label": "Rotar imagen 45 grados a la izquierda",
38692
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "-45°" }) })
38693
+ }
38694
+ ),
38695
+ /* @__PURE__ */ jsx(
38696
+ "button",
38697
+ {
38698
+ onClick: () => rotate(45),
38699
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38700
+ title: "Rotar +45°",
38701
+ disabled: creatingVariant,
38702
+ "aria-label": "Rotar imagen 45 grados a la derecha",
38703
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "+45°" }) })
38704
+ }
38705
+ ),
38706
+ /* @__PURE__ */ jsx(
38707
+ "button",
38708
+ {
38709
+ onClick: () => rotate(90),
38710
+ className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38711
+ title: "Rotar +90°",
38712
+ disabled: creatingVariant,
38713
+ "aria-label": "Rotar imagen 90 grados a la derecha",
38714
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-send-arrow-blue" }) })
38715
+ }
38716
+ )
38717
+ ] })
38718
+ ] }),
38719
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
38720
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Voltear" }),
38721
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
38722
+ /* @__PURE__ */ jsx(
38723
+ "button",
38724
+ {
38725
+ onClick: flipHorizontal,
38726
+ className: `flex-1 p-2 rounded cursor-pointer transition-colors text-sm ${flipStates.horizontal ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
38727
+ title: `Voltear horizontalmente ${flipStates.horizontal ? "(activo)" : ""}`,
38728
+ disabled: creatingVariant,
38729
+ "aria-label": "Voltear imagen horizontalmente",
38730
+ "aria-pressed": flipStates.horizontal,
38731
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-right-blue border border-brand-blue-1000 rounded" }) })
38732
+ }
38733
+ ),
38734
+ /* @__PURE__ */ jsx(
38735
+ "button",
38736
+ {
38737
+ onClick: flipVertical,
38738
+ className: `flex-1 p-2 cursor-pointer rounded transition-colors text-sm ${flipStates.vertical ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
38739
+ title: `Voltear verticalmente ${flipStates.vertical ? "(activo)" : ""}`,
38740
+ disabled: creatingVariant,
38741
+ "aria-label": "Voltear imagen verticalmente",
38742
+ "aria-pressed": flipStates.vertical,
38743
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-up-down-blue border border-brand-blue-1000 rounded" }) })
38744
+ }
38745
+ )
38746
+ ] })
38747
+ ] }),
38748
+ /* @__PURE__ */ jsx(
38749
+ "button",
38750
+ {
38751
+ onClick: () => {
38752
+ transform.reset();
38753
+ setFlipStates({ horizontal: false, vertical: false });
38754
+ },
38755
+ className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38756
+ title: "Reiniciar transformaciones",
38757
+ disabled: creatingVariant,
38758
+ "aria-label": "Reiniciar todas las transformaciones de la imagen",
38759
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
38760
+ /* @__PURE__ */ jsx("span", { className: "icon icon-refresh-blue icon--sm align-[middle!important] " }),
38761
+ " ",
38762
+ "Reiniciar ajustes"
38763
+ ] }) })
38764
+ }
38765
+ )
38766
+ ] })
38767
+ ] }),
38768
+ /* @__PURE__ */ jsxs("div", { className: "bg-gradient-to-br from-brand-blue-50 to-green-50 rounded-lg border border-blue-200 shadow-sm hidden lg:block", children: [
37561
38769
  /* @__PURE__ */ jsxs(
37562
38770
  "button",
37563
38771
  {
@@ -37586,7 +38794,7 @@ function CropperView({
37586
38794
  /* @__PURE__ */ jsxs("div", { children: [
37587
38795
  "• ",
37588
38796
  /* @__PURE__ */ jsx("strong", { children: "Arrastra la imagen:" }),
37589
- " Haz clic fuera del área de selección y arrastra"
38797
+ " Haz clic sobre la imagen del área de selección y arrastra"
37590
38798
  ] }),
37591
38799
  /* @__PURE__ */ jsxs("div", { children: [
37592
38800
  "• ",
@@ -37618,11 +38826,6 @@ function CropperView({
37618
38826
  "• ",
37619
38827
  /* @__PURE__ */ jsx("strong", { children: "Tamaños rápidos:" }),
37620
38828
  " Usa los botones 50%, 70%, 90%, 100%"
37621
- ] }),
37622
- /* @__PURE__ */ jsxs("div", { children: [
37623
- "• ",
37624
- /* @__PURE__ */ jsx("strong", { children: "Proporciones:" }),
37625
- " Selecciona ratios predefinidos (1:1, 16:9, etc.)"
37626
38829
  ] })
37627
38830
  ] })
37628
38831
  ] }),
@@ -37660,9 +38863,14 @@ function CropperView({
37660
38863
  ] }),
37661
38864
  /* @__PURE__ */ jsxs("div", { children: [
37662
38865
  "• ",
37663
- /* @__PURE__ */ jsx("strong", { children: "Grid:" }),
38866
+ /* @__PURE__ */ jsx("strong", { children: "Cuadricula:" }),
37664
38867
  " Actívalo para aplicar la regla de los tercios"
37665
38868
  ] }),
38869
+ /* @__PURE__ */ jsxs("div", { children: [
38870
+ "• ",
38871
+ /* @__PURE__ */ jsx("strong", { children: "Tablero:" }),
38872
+ " Actívalo para cuadrar medjor las medidas o como ayuda visual para imagenes con transparencia"
38873
+ ] }),
37666
38874
  /* @__PURE__ */ jsxs("div", { children: [
37667
38875
  "• ",
37668
38876
  /* @__PURE__ */ jsx("strong", { children: "Transformaciones:" }),
@@ -37691,366 +38899,139 @@ function CropperView({
37691
38899
  ] })
37692
38900
  ] })
37693
38901
  ] })
37694
- ] }),
37695
- /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: [
37696
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Proporción" }),
37697
- /* @__PURE__ */ jsx("div", { className: "limbo-cropper-aspect-buttons grid grid-cols-2 md:grid-cols-1 gap-2 md:hidden", children: allowedAspectRatios.map((ratio) => /* @__PURE__ */ jsx(
37698
- "button",
37699
- {
37700
- onClick: () => handleAspectRatio(ratio.value),
37701
- className: `p-2 text-xs rounded border transition-colors cursor-pointer ${aspectRatio === ratio.value ? "bg-blue-100 border-blue-300 text-blue-800" : "bg-gray-100 border-gray-300 text-gray-700"}`,
37702
- disabled: creatingVariant,
37703
- title: `Cambiar a proporción ${ratio.label}`,
37704
- children: ratio.label
37705
- },
37706
- ratio.value
37707
- )) }),
37708
- /* @__PURE__ */ jsx(
37709
- "select",
37710
- {
37711
- value: aspectRatio,
37712
- onChange: (e) => handleAspectRatio(e.target.value),
37713
- className: "w-full form-control hidden md:block",
37714
- disabled: creatingVariant,
37715
- "aria-label": "Seleccionar proporción de aspecto",
37716
- children: allowedAspectRatios.map((ratio) => /* @__PURE__ */ jsx("option", { value: ratio.value, children: ratio.label }, ratio.value))
37717
- }
37718
- )
37719
- ] }),
37720
- /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4", children: [
37721
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Visualización" }),
37722
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
37723
- /* @__PURE__ */ jsxs(
37724
- "button",
37725
- {
37726
- onClick: toggleGrid,
37727
- className: `w-full flex cursor-pointer items-center justify-between p-2 rounded transition-colors ${showGrid ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
37728
- disabled: creatingVariant,
37729
- "aria-pressed": showGrid,
37730
- "aria-label": "Activar/desactivar grid",
37731
- children: [
37732
- /* @__PURE__ */ jsxs("span", { className: "text-sm flex items-center", children: [
37733
- /* @__PURE__ */ jsx("span", { className: "icon icon-area-blue me-1" }),
37734
- " Grid"
37735
- ] }),
37736
- /* @__PURE__ */ jsx("span", { className: "text-xs", children: showGrid ? /* @__PURE__ */ jsxs(Fragment, { children: [
37737
- /* @__PURE__ */ jsx("span", { className: "icon icon-tick icon--xs align-[middle!important] -mt-0.5" }),
37738
- " ",
37739
- "Activo"
37740
- ] }) : "Inactivo" })
37741
- ]
37742
- }
37743
- ),
37744
- /* @__PURE__ */ jsxs(
37745
- "button",
37746
- {
37747
- onClick: toggleShade,
37748
- className: `w-full flex cursor-pointer items-center justify-between p-2 rounded transition-colors ${shade ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
37749
- disabled: creatingVariant,
37750
- "aria-pressed": shade,
37751
- "aria-label": "Activar/desactivar sombreado",
37752
- children: [
37753
- /* @__PURE__ */ jsxs("span", { className: "text-sm flex items-center", children: [
37754
- /* @__PURE__ */ jsx("span", { className: "icon icon-comparison-blue me-1" }),
37755
- " ",
37756
- "Sombreado"
37757
- ] }),
37758
- /* @__PURE__ */ jsx("span", { className: "text-xs", children: shade ? /* @__PURE__ */ jsxs(Fragment, { children: [
37759
- /* @__PURE__ */ jsx("span", { className: "icon icon-tick icon--xs align-[middle!important] -mt-0.5" }),
37760
- " ",
37761
- "Activo"
37762
- ] }) : "Inactivo" })
37763
- ]
37764
- }
37765
- )
38902
+ ] })
38903
+ ] }) })
38904
+ ] }),
38905
+ /* @__PURE__ */ jsx("div", { className: "limbo-cropper-footer flex-shrink-0 border-t border-gray-200 bg-white p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-row gap-2 sm:gap-3 items-stretch sm:items-center justify-end max-w-7xl mx-auto", children: [
38906
+ /* @__PURE__ */ jsx(
38907
+ "button",
38908
+ {
38909
+ onClick: preview,
38910
+ disabled: creatingVariant || !canExport,
38911
+ className: `px-6 py-2.5 min-h-[44px] transition-colors order-2 ${showPreview ? "limbo-btn limbo-btn-danger" : "limbo-btn limbo-btn-info"}`,
38912
+ "aria-label": "Generar vista previa del recorte",
38913
+ title: "Mostar/Ocultar vista previa del recorte",
38914
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center", children: [
38915
+ /* @__PURE__ */ jsx("span", { className: `icon md:mr-1 icon-search-white` }),
38916
+ /* @__PURE__ */ jsx("span", { className: "hidden md:block text-nowrap", children: showPreview ? "Cerrar previa" : "Vista previa" })
37766
38917
  ] })
37767
- ] }),
37768
- /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4", children: [
37769
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Ajustes del selector" }),
37770
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
38918
+ }
38919
+ ),
38920
+ /* @__PURE__ */ jsx(
38921
+ "button",
38922
+ {
38923
+ onClick: handleDownload,
38924
+ disabled: creatingVariant || !canExport,
38925
+ className: "limbo-btn limbo-btn-primary px-6 py-2.5 min-h-[44px] order-3",
38926
+ "aria-label": "Descargar recorte sin guardar",
38927
+ title: "Descargar recorte sin guardar",
38928
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center", children: [
38929
+ /* @__PURE__ */ jsx("span", { className: "icon icon-download-white md:mr-1" }),
38930
+ /* @__PURE__ */ jsx("span", { className: "hidden md:block text-nowrap", children: "Descargar" })
38931
+ ] })
38932
+ }
38933
+ ),
38934
+ /* @__PURE__ */ jsx(
38935
+ "button",
38936
+ {
38937
+ onClick: saveCrop,
38938
+ disabled: creatingVariant || !cropData || !effectiveImageInfo || !canExport || !state.isReady,
38939
+ className: "limbo-btn limbo-btn-success px-6 py-2.5 min-h-[44px] order-4",
38940
+ "aria-label": "Guardar imagen recortada",
38941
+ title: "Guardar imagen recortada",
38942
+ children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: creatingVariant ? /* @__PURE__ */ jsxs(Fragment, { children: [
38943
+ /* @__PURE__ */ jsx("span", { className: "hidden md:block text-nowrap icon icon-save-white md:mr-1" }),
38944
+ "Guardando..."
38945
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
38946
+ /* @__PURE__ */ jsx("span", { className: "icon icon-save-white md:mr-1" }),
38947
+ /* @__PURE__ */ jsx("span", { className: "hidden md:block text-nowrap", children: "Guardar recorte" })
38948
+ ] }) })
38949
+ }
38950
+ )
38951
+ ] }) }),
38952
+ showConfirmModal && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] p-4", children: /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] overflow-hidden flex flex-col", children: [
38953
+ /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 border-b border-gray-200", children: [
38954
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-800", children: pendingAction === "save" ? "Seleccionar recortes a guardar" : "Seleccionar recortes a descargar" }),
38955
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-600 mt-1", children: [
38956
+ "Marca los recortes que deseas ",
38957
+ pendingAction === "save" ? "guardar" : "descargar"
38958
+ ] })
38959
+ ] }),
38960
+ /* @__PURE__ */ jsx("div", { className: "px-6 py-4 overflow-y-auto flex-1", children: /* @__PURE__ */ jsx("div", { className: "space-y-2", children: crops.map((crop, index) => /* @__PURE__ */ jsxs(
38961
+ "label",
38962
+ {
38963
+ className: `flex items-center gap-3 p-3 rounded border cursor-pointer transition-colors ${selectedCropsForAction.includes(index) ? "bg-blue-50 border-blue-300" : "bg-gray-50 border-gray-200 hover:bg-gray-100"} ${crop.required ? "opacity-75" : ""}`,
38964
+ children: [
37771
38965
  /* @__PURE__ */ jsx(
37772
- "button",
38966
+ "input",
37773
38967
  {
37774
- onClick: centerSelection,
37775
- className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37776
- title: "Centrar selección",
37777
- disabled: creatingVariant,
37778
- "aria-label": "Centrar área de selección",
37779
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
37780
- /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button icon--sm align-[middle!important] " }),
37781
- " ",
37782
- "Centrar selección"
37783
- ] }) })
38968
+ type: "checkbox",
38969
+ checked: selectedCropsForAction.includes(index),
38970
+ disabled: crop.required,
38971
+ onChange: (e) => {
38972
+ if (e.target.checked) {
38973
+ setSelectedCropsForAction((prev) => [...prev, index]);
38974
+ } else {
38975
+ setSelectedCropsForAction((prev) => prev.filter((i) => i !== index));
38976
+ }
38977
+ },
38978
+ className: "w-4 h-4 text-blue-600"
37784
38979
  }
37785
38980
  ),
37786
- /* @__PURE__ */ jsx(
37787
- "button",
37788
- {
37789
- onClick: resetSelection,
37790
- className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37791
- title: "Reiniciar selección",
37792
- disabled: creatingVariant,
37793
- "aria-label": "Reiniciar área de selección",
37794
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
37795
- /* @__PURE__ */ jsx("span", { className: "icon icon-refresh icon--sm lign-[middle!important] " }),
37796
- " ",
37797
- "Reset selección"
37798
- ] }) })
37799
- }
37800
- )
37801
- ] }),
37802
- /* @__PURE__ */ jsxs("div", { className: "mt-3", children: [
37803
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Tamaños rápidos" }),
37804
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-1", children: [
37805
- /* @__PURE__ */ jsx(
37806
- "button",
37807
- {
37808
- onClick: () => setSelectionCoverage(0.5),
37809
- className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37810
- disabled: creatingVariant,
37811
- "aria-label": "Selección 50%",
37812
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "50%" }) })
37813
- }
37814
- ),
37815
- /* @__PURE__ */ jsx(
37816
- "button",
37817
- {
37818
- onClick: () => setSelectionCoverage(0.7),
37819
- className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37820
- disabled: creatingVariant,
37821
- "aria-label": "Selección 70%",
37822
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "70%" }) })
37823
- }
37824
- ),
37825
- /* @__PURE__ */ jsx(
37826
- "button",
37827
- {
37828
- onClick: () => setSelectionCoverage(0.9),
37829
- className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37830
- disabled: creatingVariant,
37831
- "aria-label": "Selección 90%",
37832
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "90%" }) })
37833
- }
37834
- ),
37835
- /* @__PURE__ */ jsx(
37836
- "button",
37837
- {
37838
- onClick: () => setSelectionCoverage(1),
37839
- className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37840
- disabled: creatingVariant,
37841
- "aria-label": "Selección completa",
37842
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "100%" }) })
37843
- }
37844
- )
37845
- ] })
37846
- ] })
37847
- ] }),
37848
- /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg border border-gray-200 p-4", children: [
37849
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Transformar imagen" }),
37850
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
37851
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Mover imagen" }),
37852
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-1", children: [
37853
- /* @__PURE__ */ jsx("div", {}),
37854
- /* @__PURE__ */ jsx(
37855
- "button",
37856
- {
37857
- onClick: () => move(0, -10),
37858
- className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37859
- title: "Mover arriba",
37860
- disabled: creatingVariant,
37861
- "aria-label": "Mover imagen hacia arriba",
37862
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-up-blue" }) })
37863
- }
37864
- ),
37865
- /* @__PURE__ */ jsx("div", {}),
37866
- /* @__PURE__ */ jsx(
37867
- "button",
37868
- {
37869
- onClick: () => move(-10, 0),
37870
- className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37871
- title: "Mover izquierda",
37872
- disabled: creatingVariant,
37873
- "aria-label": "Mover imagen hacia la izquierda",
37874
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-blue" }) })
37875
- }
37876
- ),
37877
- /* @__PURE__ */ jsx(
37878
- "button",
37879
- {
37880
- onClick: centerImage,
37881
- className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37882
- title: "Centrar y ajustar imagen",
37883
- disabled: creatingVariant,
37884
- "aria-label": "Centrar y ajustar imagen",
37885
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button-blue icon-md" }) })
37886
- }
37887
- ),
37888
- /* @__PURE__ */ jsx(
37889
- "button",
37890
- {
37891
- onClick: () => move(10, 0),
37892
- className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37893
- title: "Mover derecha",
37894
- disabled: creatingVariant,
37895
- "aria-label": "Mover imagen hacia la derecha",
37896
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-blue" }) })
37897
- }
37898
- ),
37899
- /* @__PURE__ */ jsx("div", {}),
37900
- /* @__PURE__ */ jsx(
37901
- "button",
37902
- {
37903
- onClick: () => move(0, 10),
37904
- className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37905
- title: "Mover abajo",
37906
- disabled: creatingVariant,
37907
- "aria-label": "Mover imagen hacia abajo",
37908
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-down-blue" }) })
37909
- }
37910
- ),
37911
- /* @__PURE__ */ jsx("div", {})
37912
- ] })
37913
- ] }),
37914
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
37915
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600 mb-2", children: [
37916
- "Zoom ",
37917
- zoomInfo ? zoomInfo.percentage + "%" : ""
37918
- ] }),
37919
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
37920
- /* @__PURE__ */ jsx(
37921
- "button",
37922
- {
37923
- onClick: () => zoom(-0.2),
37924
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37925
- title: "Alejar 20%",
37926
- disabled: creatingVariant,
37927
- "aria-label": "Alejar imagen",
37928
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-out-blue" }) })
37929
- }
37930
- ),
37931
- /* @__PURE__ */ jsx(
37932
- "button",
37933
- {
37934
- onClick: resetZoomOnly,
37935
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37936
- title: "Restablecer zoom original",
37937
- disabled: creatingVariant,
37938
- "aria-label": "Restablecer el zoom para que la imagen se vea con su resolución original",
37939
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-screenshot-blue" }) })
37940
- }
37941
- ),
37942
- /* @__PURE__ */ jsx(
37943
- "button",
37944
- {
37945
- onClick: () => zoom(0.2),
37946
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37947
- title: "Acercar 20%",
37948
- disabled: creatingVariant,
37949
- "aria-label": "Acercar imagen",
37950
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-in-blue" }) })
37951
- }
37952
- )
37953
- ] })
37954
- ] }),
37955
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
37956
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Rotación" }),
37957
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1 *:text-brand-blue-1000 *:text-sm", children: [
37958
- /* @__PURE__ */ jsx(
37959
- "button",
37960
- {
37961
- onClick: () => rotate(-90),
37962
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37963
- title: "Rotar -90°",
37964
- disabled: creatingVariant,
37965
- "aria-label": "Rotar imagen 90 grados a la izquierda",
37966
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-reply-blue" }) })
37967
- }
37968
- ),
37969
- /* @__PURE__ */ jsx(
37970
- "button",
37971
- {
37972
- onClick: () => rotate(-45),
37973
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37974
- title: "Rotar -45°",
37975
- disabled: creatingVariant,
37976
- "aria-label": "Rotar imagen 45 grados a la izquierda",
37977
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "-45°" }) })
37978
- }
37979
- ),
37980
- /* @__PURE__ */ jsx(
37981
- "button",
37982
- {
37983
- onClick: () => rotate(45),
37984
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37985
- title: "Rotar +45°",
37986
- disabled: creatingVariant,
37987
- "aria-label": "Rotar imagen 45 grados a la derecha",
37988
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "+45°" }) })
37989
- }
37990
- ),
37991
- /* @__PURE__ */ jsx(
37992
- "button",
37993
- {
37994
- onClick: () => rotate(90),
37995
- className: "flex-1 p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
37996
- title: "Rotar +90°",
37997
- disabled: creatingVariant,
37998
- "aria-label": "Rotar imagen 90 grados a la derecha",
37999
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-send-arrow-blue" }) })
38000
- }
38001
- )
38002
- ] })
38003
- ] }),
38004
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
38005
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 mb-2", children: "Voltear" }),
38006
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
38007
- /* @__PURE__ */ jsx(
38008
- "button",
38009
- {
38010
- onClick: flipHorizontal,
38011
- className: `flex-1 p-2 rounded cursor-pointer transition-colors text-sm ${flipStates.horizontal ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
38012
- title: `Voltear horizontalmente ${flipStates.horizontal ? "(activo)" : ""}`,
38013
- disabled: creatingVariant,
38014
- "aria-label": "Voltear imagen horizontalmente",
38015
- "aria-pressed": flipStates.horizontal,
38016
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-right-blue border border-brand-blue-1000 rounded" }) })
38017
- }
38018
- ),
38019
- /* @__PURE__ */ jsx(
38020
- "button",
38021
- {
38022
- onClick: flipVertical,
38023
- className: `flex-1 p-2 cursor-pointer rounded transition-colors text-sm ${flipStates.vertical ? "bg-blue-100 border border-blue-300 text-blue-800" : "bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200"}`,
38024
- title: `Voltear verticalmente ${flipStates.vertical ? "(activo)" : ""}`,
38025
- disabled: creatingVariant,
38026
- "aria-label": "Voltear imagen verticalmente",
38027
- "aria-pressed": flipStates.vertical,
38028
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-up-down-blue border border-brand-blue-1000 rounded" }) })
38029
- }
38030
- )
38981
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
38982
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38983
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-800 truncate", children: crop.label }),
38984
+ crop.required && /* @__PURE__ */ jsx("span", { className: "text-xs px-1.5 py-0.5 bg-red-100 text-red-700 rounded", children: "Obligatorio" })
38985
+ ] }),
38986
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500", children: [
38987
+ crop.width,
38988
+ " × ",
38989
+ crop.height,
38990
+ " px"
38991
+ ] })
38031
38992
  ] })
38032
- ] }),
38033
- /* @__PURE__ */ jsx(
38034
- "button",
38035
- {
38036
- onClick: () => {
38037
- transform.reset();
38038
- setFlipStates({ horizontal: false, vertical: false });
38039
- },
38040
- className: "w-full p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
38041
- title: "Reiniciar transformaciones",
38042
- disabled: creatingVariant,
38043
- "aria-label": "Reiniciar todas las transformaciones de la imagen",
38044
- children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
38045
- /* @__PURE__ */ jsx("span", { className: "icon icon-refresh-blue icon--sm align-[middle!important] " }),
38046
- " ",
38047
- "Reiniciar ajustes"
38048
- ] }) })
38049
- }
38050
- )
38051
- ] })
38052
- ] }) })
38053
- ] })
38993
+ ]
38994
+ },
38995
+ crop.id
38996
+ )) }) }),
38997
+ /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 border-t border-gray-200 flex gap-3 justify-end", children: [
38998
+ /* @__PURE__ */ jsx(
38999
+ "button",
39000
+ {
39001
+ onClick: () => {
39002
+ setShowConfirmModal(false);
39003
+ setPendingAction(null);
39004
+ setSelectedCropsForAction([]);
39005
+ },
39006
+ className: "limbo-btn limbo-btn-secondary px-4 py-2",
39007
+ children: "Cancelar"
39008
+ }
39009
+ ),
39010
+ /* @__PURE__ */ jsxs(
39011
+ "button",
39012
+ {
39013
+ onClick: async () => {
39014
+ setShowConfirmModal(false);
39015
+ if (pendingAction === "save") {
39016
+ await performSaveCrop();
39017
+ } else {
39018
+ await performDownload();
39019
+ }
39020
+ setPendingAction(null);
39021
+ setSelectedCropsForAction([]);
39022
+ },
39023
+ disabled: selectedCropsForAction.length === 0,
39024
+ className: "limbo-btn limbo-btn-primary px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed",
39025
+ children: [
39026
+ pendingAction === "save" ? "Guardar" : "Descargar",
39027
+ " (",
39028
+ selectedCropsForAction.length,
39029
+ ")"
39030
+ ]
39031
+ }
39032
+ )
39033
+ ] })
39034
+ ] }) })
38054
39035
  ] });
38055
39036
  }
38056
39037
  function Pagination({
@@ -38360,6 +39341,18 @@ function useTokenExpiration() {
38360
39341
  handleTokenExpiredClose
38361
39342
  };
38362
39343
  }
39344
+ function useDebounce(value, delay = 500) {
39345
+ const [debouncedValue, setDebouncedValue] = useState(value);
39346
+ useEffect(() => {
39347
+ const handler = setTimeout(() => {
39348
+ setDebouncedValue(value);
39349
+ }, delay);
39350
+ return () => {
39351
+ clearTimeout(handler);
39352
+ };
39353
+ }, [value, delay]);
39354
+ return debouncedValue;
39355
+ }
38363
39356
  function App({
38364
39357
  apiKey,
38365
39358
  prod = false,
@@ -38392,6 +39385,14 @@ function App({
38392
39385
  sessionStorage.removeItem("limbo_portals_images");
38393
39386
  sessionStorage.removeItem("limbo_portals_portalResults");
38394
39387
  sessionStorage.removeItem("limbo_portals_paginationInfo");
39388
+ const [galleryFilters, setGalleryFilters] = useState({
39389
+ name: "",
39390
+ uploadedBy: "",
39391
+ dateFrom: "",
39392
+ dateTo: ""
39393
+ });
39394
+ const debouncedName = useDebounce(galleryFilters.name, 500);
39395
+ const debouncedUploadedBy = useDebounce(galleryFilters.uploadedBy, 500);
38395
39396
  const getFilteredFeatures = () => {
38396
39397
  switch (modeUI) {
38397
39398
  case "gallery-only":
@@ -38435,6 +39436,14 @@ function App({
38435
39436
  error: deleteError,
38436
39437
  reset: resetDelete
38437
39438
  } = useDeleteImage();
39439
+ const apiParams = {
39440
+ limit: itemsPerPage,
39441
+ page: currentPage,
39442
+ ...debouncedName && { name: debouncedName },
39443
+ ...debouncedUploadedBy && { uploadedBy: debouncedUploadedBy },
39444
+ ...galleryFilters.dateFrom && { dateFrom: galleryFilters.dateFrom },
39445
+ ...galleryFilters.dateTo && { dateTo: galleryFilters.dateTo }
39446
+ };
38438
39447
  const {
38439
39448
  images,
38440
39449
  loading: loadingImages,
@@ -38442,7 +39451,7 @@ function App({
38442
39451
  pagination,
38443
39452
  invalidateCache,
38444
39453
  setImages
38445
- } = useImages(apiKey, prod, { limit: itemsPerPage, page: currentPage });
39454
+ } = useImages(apiKey, prod, apiParams);
38446
39455
  const { refreshVariants } = useImageVariants();
38447
39456
  const handleVariantCreated = (assetId, variantData) => {
38448
39457
  refreshVariants(assetId);
@@ -38486,7 +39495,7 @@ function App({
38486
39495
  }
38487
39496
  };
38488
39497
  const handleDelete = async (imageId) => {
38489
- if (!confirm("¿Estás seguro de que quieres eliminar esta imagen?")) {
39498
+ if (!confirm("¿Estás seguro de que deseas eliminar esta imagen? Esta acción también eliminará todos sus recortes.")) {
38490
39499
  return;
38491
39500
  }
38492
39501
  const success = await deleteImg(imageId);
@@ -38524,8 +39533,148 @@ function App({
38524
39533
  if (tabId !== "upload") resetUpload();
38525
39534
  if (tabId !== "gallery") resetDelete();
38526
39535
  };
38527
- return /* @__PURE__ */ jsxs("div", { className: "limbo-card-parent max-w-7xl mx-auto my-8 p-6 shadow-xl w-full", children: [
38528
- ui.showTabs && /* @__PURE__ */ jsx(Tabs, { tabs, active: activeTab, onChange: handleTabChange }),
39536
+ const determineScenario = () => {
39537
+ if (modeUI === "crop-only" || activeFeatures.length === 1 && activeFeatures[0] === "cropper") {
39538
+ return "crop-only";
39539
+ }
39540
+ if (activeFeatures.includes("gallery")) {
39541
+ return "with-gallery";
39542
+ }
39543
+ if (activeFeatures.includes("upload") && !activeFeatures.includes("gallery")) {
39544
+ return "upload-only";
39545
+ }
39546
+ return "with-gallery";
39547
+ };
39548
+ const handleCropSave = (result) => {
39549
+ const scenario = determineScenario();
39550
+ const globalConfig2 = window.limboCore?.config?.getGlobal() || {};
39551
+ const mode = globalConfig2.mode || "embed";
39552
+ const autoHide = globalConfig2.autoHideOnComplete || false;
39553
+ const crops = Array.isArray(result) ? result : [result];
39554
+ if (scenario === "with-gallery") {
39555
+ invalidateCache();
39556
+ }
39557
+ if (callbacks.onCropsSaved) {
39558
+ callbacks.onCropsSaved({
39559
+ crops,
39560
+ assetId: selectedImage?.id,
39561
+ instanceId
39562
+ });
39563
+ }
39564
+ if (window.limboCore?.events) {
39565
+ window.limboCore.events.emit("cropsSaved", {
39566
+ crops,
39567
+ assetId: selectedImage?.id,
39568
+ instanceId
39569
+ });
39570
+ }
39571
+ switch (scenario) {
39572
+ case "with-gallery":
39573
+ setSelectedImage(null);
39574
+ setActiveTab("gallery");
39575
+ break;
39576
+ case "upload-only":
39577
+ if (mode === "modal") {
39578
+ window.limboCore?.modals?.closeAllModals();
39579
+ } else {
39580
+ setSelectedImage(null);
39581
+ setActiveTab("upload");
39582
+ }
39583
+ break;
39584
+ case "crop-only":
39585
+ if (mode === "modal") {
39586
+ if (callbacks.onCropperComplete) {
39587
+ callbacks.onCropperComplete({
39588
+ crops,
39589
+ instanceId
39590
+ });
39591
+ }
39592
+ if (window.limboCore?.events) {
39593
+ window.limboCore.events.emit("cropperComplete", {
39594
+ crops,
39595
+ instanceId
39596
+ });
39597
+ }
39598
+ window.limboCore?.modals?.closeAllModals();
39599
+ } else {
39600
+ if (callbacks.onCropperComplete) {
39601
+ callbacks.onCropperComplete({
39602
+ crops,
39603
+ instanceId
39604
+ });
39605
+ }
39606
+ if (window.limboCore?.events) {
39607
+ window.limboCore.events.emit("cropperComplete", {
39608
+ crops,
39609
+ instanceId
39610
+ });
39611
+ }
39612
+ if (autoHide) {
39613
+ const container = document.querySelector(`#limbo-instance-${instanceId}`);
39614
+ if (container) container.style.display = "none";
39615
+ }
39616
+ }
39617
+ break;
39618
+ }
39619
+ };
39620
+ const handleCropCancel = () => {
39621
+ const scenario = determineScenario();
39622
+ const globalConfig2 = window.limboCore?.config?.getGlobal() || {};
39623
+ const mode = globalConfig2.mode || "embed";
39624
+ if (callbacks.onCropperCancelled) {
39625
+ callbacks.onCropperCancelled({
39626
+ assetId: selectedImage?.id,
39627
+ instanceId
39628
+ });
39629
+ }
39630
+ if (window.limboCore?.events) {
39631
+ window.limboCore.events.emit("cropperCancelled", {
39632
+ assetId: selectedImage?.id,
39633
+ instanceId
39634
+ });
39635
+ }
39636
+ switch (scenario) {
39637
+ case "with-gallery":
39638
+ setSelectedImage(null);
39639
+ setActiveTab("gallery");
39640
+ break;
39641
+ case "upload-only":
39642
+ if (mode === "modal") {
39643
+ window.limboCore?.modals?.closeAllModals();
39644
+ } else {
39645
+ setSelectedImage(null);
39646
+ setActiveTab("upload");
39647
+ }
39648
+ break;
39649
+ case "crop-only":
39650
+ if (mode === "modal") {
39651
+ window.limboCore?.modals?.closeAllModals();
39652
+ } else {
39653
+ if (window.limboCore?.events) {
39654
+ window.limboCore.events.emit("cropperClosed", {
39655
+ instanceId,
39656
+ reason: "cancelled"
39657
+ });
39658
+ }
39659
+ }
39660
+ break;
39661
+ }
39662
+ };
39663
+ const handleCropError = (error) => {
39664
+ if (callbacks.onCropperError) {
39665
+ callbacks.onCropperError(error, instanceId);
39666
+ }
39667
+ if (window.limboCore?.events) {
39668
+ window.limboCore.events.emit("cropperError", {
39669
+ error,
39670
+ assetId: selectedImage?.id,
39671
+ instanceId
39672
+ });
39673
+ }
39674
+ console.error("Cropper error:", error);
39675
+ };
39676
+ return /* @__PURE__ */ jsxs("div", { className: "limbo-card-parent max-w-7xl mx-auto my-8 p-6 shadow-xs w-full", children: [
39677
+ ui.showTabs && activeTab != "cropper" && /* @__PURE__ */ jsx(Tabs, { tabs, active: activeTab, onChange: handleTabChange }),
38529
39678
  imagesError && /* @__PURE__ */ jsxs("div", { className: "alert alert-danger", children: [
38530
39679
  "Error al cargar imágenes: ",
38531
39680
  imagesError
@@ -38539,8 +39688,6 @@ function App({
38539
39688
  /* @__PURE__ */ jsx(
38540
39689
  Gallery,
38541
39690
  {
38542
- apiKey,
38543
- prod,
38544
39691
  onSelect: handleImageSelect,
38545
39692
  onCrop: handleImageCrop,
38546
39693
  onDelete: isActionAllowed("delete") ? handleDelete : null,
@@ -38548,6 +39695,13 @@ function App({
38548
39695
  images,
38549
39696
  loading: loadingImages,
38550
39697
  error: imagesError,
39698
+ filters: galleryFilters,
39699
+ onFiltersChange: (newFilters) => {
39700
+ setGalleryFilters(newFilters);
39701
+ setCurrentPage(1);
39702
+ },
39703
+ filterConfig: ui.galleryFilters || {},
39704
+ loadingConfig: ui.galleryLoading || {},
38551
39705
  allowedActions: {
38552
39706
  select: isActionAllowed("select"),
38553
39707
  download: isActionAllowed("download"),
@@ -38591,16 +39745,10 @@ function App({
38591
39745
  CropperView,
38592
39746
  {
38593
39747
  image: selectedImage,
38594
- onSave: () => {
38595
- invalidateCache();
38596
- setSelectedImage(null);
38597
- setActiveTab("gallery");
38598
- },
38599
- onCancel: () => {
38600
- setSelectedImage(null);
38601
- setActiveTab("gallery");
38602
- },
39748
+ onSave: handleCropSave,
39749
+ onCancel: handleCropCancel,
38603
39750
  onDelete: () => handleDelete(selectedImage?.id),
39751
+ onError: handleCropError,
38604
39752
  deleting,
38605
39753
  onVariantCreated: handleVariantCreated
38606
39754
  }
@@ -38905,7 +40053,12 @@ class LimboInstance {
38905
40053
  id: `limbo-component-container-${this.id}`,
38906
40054
  className: "limbo-instance limbo-component-container-wrapper py-3",
38907
40055
  "data-limbo-id": this.id,
38908
- "data-limbo-mode": this.config.mode
40056
+ "data-limbo-mode": this.config.mode,
40057
+ "data-limbo-version": "2.0",
40058
+ "data-limbo-isolated": "true",
40059
+ // Agregar aria-label para accesibilidad
40060
+ "aria-label": "Limbo Image Manager",
40061
+ "role": "region"
38909
40062
  },
38910
40063
  React.createElement(App, {
38911
40064
  apiKey: this.config.apiKey || null,
@@ -41925,6 +43078,288 @@ class LimboCore {
41925
43078
  });
41926
43079
  modalInstance.open();
41927
43080
  }
43081
+ // ========================================
43082
+ // MÉTODOS PREFAB - CASOS DE USO COMUNES
43083
+ // ========================================
43084
+ /**
43085
+ * 🎨 Abrir galería de imágenes en modal
43086
+ * Caso de uso: Selector rápido de imágenes existentes
43087
+ *
43088
+ * @param {Object} options - Opciones de configuración
43089
+ * @param {Function} options.onSelect - Callback cuando se selecciona imagen
43090
+ * @param {Object} options.filters - Filtros pre-aplicados
43091
+ * @returns {Object} Instancia del modal
43092
+ *
43093
+ * @example
43094
+ * Limbo.openGallery({
43095
+ * onSelect: (image) => console.log('Imagen seleccionada:', image)
43096
+ * });
43097
+ */
43098
+ openGallery(options = {}) {
43099
+ const instance = this.create({
43100
+ mode: "modal",
43101
+ modeUI: "gallery-only",
43102
+ features: ["gallery"],
43103
+ autoDestroy: true,
43104
+ modal: {
43105
+ title: options.title || "Seleccionar Imagen",
43106
+ size: options.size || "large"
43107
+ },
43108
+ callbacks: {
43109
+ onSelect: (imageData) => {
43110
+ options.onSelect?.(imageData);
43111
+ if (options.autoClose !== false) {
43112
+ instance.close();
43113
+ }
43114
+ }
43115
+ },
43116
+ ...options
43117
+ });
43118
+ instance.open();
43119
+ return instance;
43120
+ }
43121
+ /**
43122
+ * 📤 Abrir uploader en modal
43123
+ * Caso de uso: Subida rápida de nueva imagen
43124
+ *
43125
+ * @param {Object} options - Opciones de configuración
43126
+ * @param {Function} options.onUpload - Callback cuando se sube imagen
43127
+ * @param {Boolean} options.redirectToGallery - Si true, muestra galería tras subir
43128
+ * @returns {Object} Instancia del modal
43129
+ *
43130
+ * @example
43131
+ * Limbo.openUploader({
43132
+ * onUpload: (image) => console.log('Imagen subida:', image),
43133
+ * redirectToGallery: false
43134
+ * });
43135
+ */
43136
+ openUploader(options = {}) {
43137
+ const features = options.redirectToGallery !== false ? ["upload", "gallery"] : ["upload"];
43138
+ const instance = this.create({
43139
+ mode: "modal",
43140
+ modeUI: options.redirectToGallery !== false ? "full" : "upload-only",
43141
+ features,
43142
+ autoDestroy: true,
43143
+ modal: {
43144
+ title: options.title || "Subir Imagen",
43145
+ size: options.size || "medium"
43146
+ },
43147
+ callbacks: {
43148
+ onUpload: (imageData) => {
43149
+ options.onUpload?.(imageData);
43150
+ if (options.autoClose !== false && !options.redirectToGallery) {
43151
+ instance.close();
43152
+ }
43153
+ }
43154
+ },
43155
+ ...options
43156
+ });
43157
+ instance.open();
43158
+ return instance;
43159
+ }
43160
+ /**
43161
+ * ✂️ Abrir cropper standalone con imagen externa
43162
+ * Caso de uso: Recortar una imagen específica sin galería
43163
+ *
43164
+ * @param {String} imageUrl - URL de la imagen a recortar
43165
+ * @param {Object} options - Opciones de configuración
43166
+ * @param {Array} options.mandatoryCrops - Recortes obligatorios
43167
+ * @param {Function} options.onComplete - Callback cuando se completan los recortes
43168
+ * @param {Function} options.onCancelled - Callback cuando se cancela
43169
+ * @returns {Object} Instancia del modal
43170
+ *
43171
+ * @example
43172
+ * Limbo.openCropper('https://example.com/image.jpg', {
43173
+ * mandatoryCrops: [
43174
+ * { label: 'Thumbnail', width: 300, height: 300, required: true },
43175
+ * { label: 'Header', width: 1920, height: 400, required: true }
43176
+ * ],
43177
+ * onComplete: (crops) => console.log('Recortes:', crops),
43178
+ * onCancelled: () => console.log('Cancelado')
43179
+ * });
43180
+ */
43181
+ openCropper(imageUrl, options = {}) {
43182
+ if (!imageUrl) {
43183
+ throw new Error("Limbo.openCropper: imageUrl es requerido");
43184
+ }
43185
+ const imageObject = {
43186
+ url: imageUrl,
43187
+ filename: options.filename || "image.jpg",
43188
+ width: options.width || 1920,
43189
+ height: options.height || 1080,
43190
+ mime_type: options.mimeType || "image/jpeg",
43191
+ id: options.assetId || `external-${Date.now()}`
43192
+ };
43193
+ const instance = this.create({
43194
+ mode: "modal",
43195
+ modeUI: "crop-only",
43196
+ features: ["cropper"],
43197
+ autoDestroy: true,
43198
+ modal: {
43199
+ title: options.title || "Recortar Imagen",
43200
+ size: "xlarge"
43201
+ },
43202
+ cropper: {
43203
+ mandatoryCrops: options.mandatoryCrops || [],
43204
+ allowCustomCrops: options.allowCustomCrops !== false,
43205
+ enforceRequiredCrops: options.enforceRequiredCrops !== false
43206
+ },
43207
+ callbacks: {
43208
+ onCropperComplete: (data) => {
43209
+ options.onComplete?.(data);
43210
+ if (options.autoClose !== false) {
43211
+ instance.close();
43212
+ }
43213
+ },
43214
+ onCropperCancelled: (data) => {
43215
+ options.onCancelled?.(data);
43216
+ if (options.autoClose !== false) {
43217
+ instance.close();
43218
+ }
43219
+ },
43220
+ onCropperError: (error) => {
43221
+ options.onError?.(error);
43222
+ }
43223
+ },
43224
+ // Pasar imagen al componente
43225
+ _externalImage: imageObject,
43226
+ ...options
43227
+ });
43228
+ instance.open();
43229
+ return instance;
43230
+ }
43231
+ /**
43232
+ * 🖼️ Crear galería embebida en un contenedor
43233
+ * Caso de uso: Galería permanente en una página
43234
+ *
43235
+ * @param {String|HTMLElement} container - Selector o elemento del contenedor
43236
+ * @param {Object} options - Opciones de configuración
43237
+ * @returns {Object} Instancia del componente
43238
+ *
43239
+ * @example
43240
+ * Limbo.createInlineGallery('#gallery-container', {
43241
+ * onSelect: (image) => console.log('Seleccionada:', image)
43242
+ * });
43243
+ */
43244
+ createInlineGallery(container, options = {}) {
43245
+ return this.create({
43246
+ container,
43247
+ mode: "embed",
43248
+ modeUI: "gallery-only",
43249
+ features: ["gallery"],
43250
+ callbacks: {
43251
+ onSelect: options.onSelect,
43252
+ onDelete: options.onDelete
43253
+ },
43254
+ ...options
43255
+ });
43256
+ }
43257
+ /**
43258
+ * 📤 Crear uploader embebido en un contenedor
43259
+ * Caso de uso: Formulario de subida permanente
43260
+ *
43261
+ * @param {String|HTMLElement} container - Selector o elemento del contenedor
43262
+ * @param {Object} options - Opciones de configuración
43263
+ * @returns {Object} Instancia del componente
43264
+ *
43265
+ * @example
43266
+ * Limbo.createInlineUploader('#upload-container', {
43267
+ * onUpload: (image) => console.log('Subida:', image)
43268
+ * });
43269
+ */
43270
+ createInlineUploader(container, options = {}) {
43271
+ return this.create({
43272
+ container,
43273
+ mode: "embed",
43274
+ modeUI: "upload-only",
43275
+ features: ["upload"],
43276
+ callbacks: {
43277
+ onUpload: options.onUpload
43278
+ },
43279
+ ...options
43280
+ });
43281
+ }
43282
+ /**
43283
+ * ✂️ Crear cropper embebido con imagen externa
43284
+ * Caso de uso: Editor de recortes embebido en página
43285
+ *
43286
+ * @param {String|HTMLElement} container - Selector o elemento del contenedor
43287
+ * @param {String} imageUrl - URL de la imagen a recortar
43288
+ * @param {Object} options - Opciones de configuración
43289
+ * @returns {Object} Instancia del componente
43290
+ *
43291
+ * @example
43292
+ * Limbo.createStandaloneCropper('#cropper-container', 'https://example.com/image.jpg', {
43293
+ * mandatoryCrops: [{ label: 'Avatar', width: 200, height: 200, required: true }],
43294
+ * onComplete: (crops) => console.log('Recortes completados:', crops),
43295
+ * autoHideOnComplete: true
43296
+ * });
43297
+ */
43298
+ createStandaloneCropper(container, imageUrl, options = {}) {
43299
+ if (!container) {
43300
+ throw new Error("Limbo.createStandaloneCropper: container es requerido");
43301
+ }
43302
+ if (!imageUrl) {
43303
+ throw new Error("Limbo.createStandaloneCropper: imageUrl es requerido");
43304
+ }
43305
+ const imageObject = {
43306
+ url: imageUrl,
43307
+ filename: options.filename || "image.jpg",
43308
+ width: options.width || 1920,
43309
+ height: options.height || 1080,
43310
+ mime_type: options.mimeType || "image/jpeg",
43311
+ id: options.assetId || `external-${Date.now()}`
43312
+ };
43313
+ return this.create({
43314
+ container,
43315
+ mode: "embed",
43316
+ modeUI: "crop-only",
43317
+ features: ["cropper"],
43318
+ autoHideOnComplete: options.autoHideOnComplete !== false,
43319
+ cropper: {
43320
+ mandatoryCrops: options.mandatoryCrops || [],
43321
+ allowCustomCrops: options.allowCustomCrops !== false,
43322
+ enforceRequiredCrops: options.enforceRequiredCrops !== false
43323
+ },
43324
+ callbacks: {
43325
+ onCropperComplete: options.onComplete,
43326
+ onCropperCancelled: options.onCancelled,
43327
+ onCropperError: options.onError
43328
+ },
43329
+ // Pasar imagen al componente
43330
+ _externalImage: imageObject,
43331
+ ...options
43332
+ });
43333
+ }
43334
+ /**
43335
+ * 🎯 Crear selector completo (galería + uploader + cropper)
43336
+ * Caso de uso: Selector completo con todas las funcionalidades
43337
+ *
43338
+ * @param {String|HTMLElement} container - Selector o elemento del contenedor
43339
+ * @param {Object} options - Opciones de configuración
43340
+ * @returns {Object} Instancia del componente
43341
+ *
43342
+ * @example
43343
+ * Limbo.createFullSelector('#selector', {
43344
+ * onSelect: (image) => document.getElementById('preview').src = image.url,
43345
+ * onUpload: (image) => console.log('Nueva imagen:', image)
43346
+ * });
43347
+ */
43348
+ createFullSelector(container, options = {}) {
43349
+ return this.create({
43350
+ container,
43351
+ mode: options.mode || "embed",
43352
+ modeUI: "full",
43353
+ features: ["gallery", "upload", "cropper"],
43354
+ callbacks: {
43355
+ onSelect: options.onSelect,
43356
+ onUpload: options.onUpload,
43357
+ onDelete: options.onDelete,
43358
+ onCropsSaved: options.onCropsSaved
43359
+ },
43360
+ ...options
43361
+ });
43362
+ }
41928
43363
  }
41929
43364
  const Limbo = new LimboCore();
41930
43365
  if (typeof window !== "undefined") {