limbo-component 1.0.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +45 -10
  2. package/LICENSE +13 -32
  3. package/README.md +63 -33
  4. package/dist/limbo.cjs.js +5 -5
  5. package/dist/limbo.cjs.map +1 -1
  6. package/dist/limbo.css +1 -1
  7. package/dist/limbo.es.js +2501 -788
  8. package/dist/limbo.es.map +1 -1
  9. package/dist/limbo.min.js +6 -6
  10. package/dist/limbo.min.js.map +1 -1
  11. package/dist/limbo.umd.js +8 -8
  12. package/dist/limbo.umd.js.map +1 -1
  13. package/dist/types/App.d.ts +2 -1
  14. package/dist/types/App.d.ts.map +1 -1
  15. package/dist/types/components/CropperView.d.ts +2 -3
  16. package/dist/types/components/CropperView.d.ts.map +1 -1
  17. package/dist/types/components/Gallery.d.ts +1 -0
  18. package/dist/types/components/Gallery.d.ts.map +1 -1
  19. package/dist/types/components/ImageCard.d.ts +1 -0
  20. package/dist/types/components/ImageCard.d.ts.map +1 -1
  21. package/dist/types/components/ImageVariantsModal.d.ts +17 -0
  22. package/dist/types/components/ImageVariantsModal.d.ts.map +1 -0
  23. package/dist/types/components/Pagination.d.ts +7 -0
  24. package/dist/types/components/Pagination.d.ts.map +1 -0
  25. package/dist/types/components/TabAI.d.ts +2 -2
  26. package/dist/types/components/TabAI.d.ts.map +1 -1
  27. package/dist/types/components/TabPortals.d.ts +2 -2
  28. package/dist/types/components/TabPortals.d.ts.map +1 -1
  29. package/dist/types/components/TabStock.d.ts +1 -2
  30. package/dist/types/components/TabStock.d.ts.map +1 -1
  31. package/dist/types/components/TokenExpiredModal.d.ts +9 -0
  32. package/dist/types/components/TokenExpiredModal.d.ts.map +1 -0
  33. package/dist/types/components/UploadForm.d.ts.map +1 -1
  34. package/dist/types/core/LimboCore.d.ts +4 -0
  35. package/dist/types/core/LimboCore.d.ts.map +1 -1
  36. package/dist/types/hooks/useAiServices.d.ts +1 -1
  37. package/dist/types/hooks/useAiServices.d.ts.map +1 -1
  38. package/dist/types/hooks/useCreateVariant.d.ts +13 -0
  39. package/dist/types/hooks/useCreateVariant.d.ts.map +1 -0
  40. package/dist/types/hooks/useExternalImages.d.ts +9 -1
  41. package/dist/types/hooks/useExternalImages.d.ts.map +1 -1
  42. package/dist/types/hooks/useImageParams.d.ts +1 -1
  43. package/dist/types/hooks/useImageParams.d.ts.map +1 -1
  44. package/dist/types/hooks/useImageVariants.d.ts +20 -0
  45. package/dist/types/hooks/useImageVariants.d.ts.map +1 -0
  46. package/dist/types/hooks/useImages.d.ts +1 -0
  47. package/dist/types/hooks/useImages.d.ts.map +1 -1
  48. package/dist/types/hooks/useIsAllowedAll.d.ts.map +1 -1
  49. package/dist/types/hooks/usePortalSources.d.ts +1 -1
  50. package/dist/types/hooks/usePortalSources.d.ts.map +1 -1
  51. package/dist/types/hooks/useStockServices.d.ts +1 -1
  52. package/dist/types/hooks/useStockServices.d.ts.map +1 -1
  53. package/dist/types/hooks/useTokenExpiration.d.ts +10 -0
  54. package/dist/types/hooks/useTokenExpiration.d.ts.map +1 -0
  55. package/dist/types/services/aiApi.d.ts +4 -2
  56. package/dist/types/services/aiApi.d.ts.map +1 -1
  57. package/dist/types/services/apiClient.d.ts +22 -12
  58. package/dist/types/services/apiClient.d.ts.map +1 -1
  59. package/dist/types/services/assetsApi.d.ts +13 -6
  60. package/dist/types/services/assetsApi.d.ts.map +1 -1
  61. package/dist/types/services/imageParamsApi.d.ts +1 -1
  62. package/dist/types/services/imageParamsApi.d.ts.map +1 -1
  63. package/dist/types/services/portalsApi.d.ts +2 -2
  64. package/dist/types/services/portalsApi.d.ts.map +1 -1
  65. package/dist/types/services/responseAdapters.d.ts +6 -6
  66. package/dist/types/services/responseAdapters.d.ts.map +1 -1
  67. package/dist/types/services/stockApi.d.ts +3 -3
  68. package/dist/types/services/stockApi.d.ts.map +1 -1
  69. package/package.json +11 -3
  70. package/dist/types/services/authService.d.ts +0 -65
  71. package/dist/types/services/authService.d.ts.map +0 -1
package/dist/limbo.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
1
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
2
  import require$$2 from "react-dom";
3
3
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
4
4
  const DEFAULT_MESSAGES = {
@@ -30452,6 +30452,937 @@ function Tabs({ tabs, active, onChange }) {
30452
30452
  }
30453
30453
  );
30454
30454
  }
30455
+ const API_URLS = {
30456
+ // DEV: "https://led-dev-limbo-dev.eu.els.local", // PREPRODUCCIÓN - Updated URL
30457
+ DEV: "http://localhost",
30458
+ // LOCAL - Para desarrollo local
30459
+ PROD: "https://limbo.lefebvre.com"
30460
+ };
30461
+ let globalConfig = {
30462
+ publicKey: null,
30463
+ token: null,
30464
+ // JWT token (opcional, generado automáticamente)
30465
+ authMode: null,
30466
+ // "session" | "manual"
30467
+ tokenEndpoint: null,
30468
+ // Endpoint para obtener token (configurable)
30469
+ prod: false
30470
+ };
30471
+ function configureApiClient(config) {
30472
+ globalConfig = {
30473
+ ...globalConfig,
30474
+ ...config
30475
+ };
30476
+ if (!globalConfig.authMode) {
30477
+ if (globalConfig.token) {
30478
+ globalConfig.authMode = "manual";
30479
+ } else {
30480
+ globalConfig.authMode = "session";
30481
+ }
30482
+ }
30483
+ }
30484
+ function getBaseUrl({ prod = false } = {}) {
30485
+ return prod ? API_URLS.PROD : API_URLS.DEV;
30486
+ }
30487
+ async function getHeaders({ isFormData = false, useJWT = true, customHeaders = {} } = {}) {
30488
+ const headers = {};
30489
+ if (!isFormData) {
30490
+ headers["Content-Type"] = "application/json";
30491
+ }
30492
+ if (useJWT) {
30493
+ let token = globalConfig.token;
30494
+ if (globalConfig.authMode === "session" && !token) {
30495
+ try {
30496
+ const baseUrl = getBaseUrl(globalConfig);
30497
+ const endpoint = globalConfig.tokenEndpoint || "/auth/token";
30498
+ const response = await fetch(`${baseUrl}${endpoint}`, {
30499
+ method: "POST",
30500
+ headers: { "Content-Type": "application/json" },
30501
+ body: JSON.stringify({ public_key: globalConfig.publicKey }),
30502
+ credentials: "include"
30503
+ // Incluir cookies de sesión
30504
+ });
30505
+ if (response.ok) {
30506
+ const data = await response.json();
30507
+ token = data.token;
30508
+ globalConfig.token = token;
30509
+ } else {
30510
+ throw new Error(`Session auth failed: ${response.status}`);
30511
+ }
30512
+ } catch (error) {
30513
+ console.error("❌ Session authentication failed:", error);
30514
+ throw new Error("Failed to authenticate with session. User must be logged in.");
30515
+ }
30516
+ }
30517
+ if (token) {
30518
+ headers.Authorization = `Bearer ${token}`;
30519
+ } else {
30520
+ console.warn("⚠️ No JWT token available:", {
30521
+ authMode: globalConfig.authMode,
30522
+ hasPublicKey: !!globalConfig.publicKey
30523
+ });
30524
+ }
30525
+ }
30526
+ if (isFormData) {
30527
+ delete headers["Content-Type"];
30528
+ }
30529
+ return { ...headers, ...customHeaders };
30530
+ }
30531
+ function setToken(token) {
30532
+ globalConfig.token = token;
30533
+ }
30534
+ function getConfig() {
30535
+ return { ...globalConfig };
30536
+ }
30537
+ async function callApi({
30538
+ endpoint,
30539
+ method = "GET",
30540
+ body = null,
30541
+ prod = false,
30542
+ basePath = "",
30543
+ customHeaders = {},
30544
+ isFormData = false,
30545
+ useJWT = true
30546
+ // New parameter to control JWT usage
30547
+ }) {
30548
+ try {
30549
+ const useProd = prod || globalConfig.prod;
30550
+ const headers = await getHeaders({
30551
+ isFormData,
30552
+ useJWT,
30553
+ customHeaders
30554
+ });
30555
+ if (method === "GET" && !body && headers["Content-Type"]) {
30556
+ delete headers["Content-Type"];
30557
+ }
30558
+ const res = await fetch(`${getBaseUrl({ prod: useProd })}${basePath}${endpoint}`, {
30559
+ method,
30560
+ headers,
30561
+ body: body ? isFormData ? body : JSON.stringify(body) : void 0,
30562
+ credentials: "include"
30563
+ // Include cookies for CORS
30564
+ });
30565
+ if (!res.ok) {
30566
+ let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
30567
+ let errorData = null;
30568
+ try {
30569
+ const result = await res.json();
30570
+ errorData = result;
30571
+ if (result.error) {
30572
+ errorMessage = `${result.error.code}: ${result.error.message}`;
30573
+ } else if (result.message) {
30574
+ errorMessage = result.message;
30575
+ }
30576
+ } catch {
30577
+ }
30578
+ if (res.status === 401 && errorData) {
30579
+ if (errorData.error === "token_expired" || errorData.message && errorData.message.includes("expired") || errorData.message && errorData.message.includes("caducado") || errorData.error && errorData.error.includes("expired")) {
30580
+ window.dispatchEvent(new CustomEvent("tokenExpiredError", {
30581
+ detail: { error: { response: { status: 401, data: errorData } } }
30582
+ }));
30583
+ }
30584
+ }
30585
+ throw new Error(errorMessage);
30586
+ }
30587
+ return res.json();
30588
+ } catch (err) {
30589
+ throw new Error(`API Error: ${err.message}`);
30590
+ }
30591
+ }
30592
+ function getApiBaseUrl() {
30593
+ const config = getConfig();
30594
+ const prod = config?.prod || false;
30595
+ const API_URLS2 = {
30596
+ DEV: "http://localhost",
30597
+ PROD: "https://limbo.lefebvre.com"
30598
+ };
30599
+ return prod ? API_URLS2.PROD : API_URLS2.DEV;
30600
+ }
30601
+ function generateFallbackAssetUrl(asset) {
30602
+ if (!asset?.id) return null;
30603
+ const baseUrl = getApiBaseUrl();
30604
+ return `${baseUrl}/api/assets/${asset.id}/download`;
30605
+ }
30606
+ function makeAbsoluteUrl(url) {
30607
+ if (!url) return null;
30608
+ if (url.startsWith("http://") || url.startsWith("https://")) {
30609
+ return url;
30610
+ }
30611
+ if (url.startsWith("/")) {
30612
+ const baseUrl = getApiBaseUrl();
30613
+ return `${baseUrl}${url}`;
30614
+ }
30615
+ return url;
30616
+ }
30617
+ function adaptAssetsListFromV2(v2Response) {
30618
+ if (!v2Response?.success || !Array.isArray(v2Response?.data?.data)) {
30619
+ return { result: [] };
30620
+ }
30621
+ const legacyAssets = v2Response.data.data.map((asset) => {
30622
+ return {
30623
+ id: asset.id,
30624
+ filename: asset.filename || asset.original_filename,
30625
+ mime_type: asset.mime_type,
30626
+ file_size: asset.file_size,
30627
+ width: asset.width,
30628
+ height: asset.height,
30629
+ upload_date: asset.upload_date || asset.created_at,
30630
+ processing_status: asset.processing_status || asset.status,
30631
+ // URL principal - handle multiple URL formats from different backend responses
30632
+ url: makeAbsoluteUrl(
30633
+ asset.master_url || asset.master?.url_signed || asset.urls?.original || asset.url
30634
+ ) || generateFallbackAssetUrl(asset),
30635
+ // Generate fallback URL if none provided
30636
+ webp_available: asset.webp_available || !!asset.webp_url,
30637
+ // Solo metadatos básicos en listado
30638
+ metadata: asset.metadata || {},
30639
+ variants_count: asset.variants_count || 0
30640
+ };
30641
+ });
30642
+ const response = { result: legacyAssets };
30643
+ if (v2Response.data.pagination) {
30644
+ response.pagination = {
30645
+ page: v2Response.data.pagination.page,
30646
+ limit: v2Response.data.pagination.limit,
30647
+ total: v2Response.data.pagination.total,
30648
+ pages: v2Response.data.pagination.pages
30649
+ };
30650
+ }
30651
+ return response;
30652
+ }
30653
+ function adaptVariantsListFromV2(v2Response) {
30654
+ if (!v2Response?.success || !Array.isArray(v2Response?.data?.variants)) {
30655
+ return { result: [] };
30656
+ }
30657
+ const legacyVariants = v2Response.data.variants.map((variant) => {
30658
+ return {
30659
+ id: variant.id,
30660
+ name: variant.filename || variant.name || `Variante ${variant.id}`,
30661
+ filename: variant.filename,
30662
+ mime_type: variant.mime_type,
30663
+ format: variant.output_format || variant.format || (variant.mime_type ? variant.mime_type.split("/")[1] : "jpg"),
30664
+ file_size: variant.file_size,
30665
+ width: variant.width,
30666
+ height: variant.height,
30667
+ upload_date: variant.upload_date || variant.created_at,
30668
+ processing_status: variant.processing_status || variant.status,
30669
+ // URL principal - handle multiple URL formats from different backend responses
30670
+ url: makeAbsoluteUrl(
30671
+ variant.master_url || variant.master?.url_signed || variant.urls?.original || variant.url
30672
+ ) || generateFallbackAssetUrl(variant),
30673
+ webp_available: variant.webp_available || !!variant.webp_url,
30674
+ // Metadatos específicos de variante
30675
+ metadata: variant.metadata || {},
30676
+ crop_data: variant.crop_data || {},
30677
+ crop_params: variant.crop_params || variant.crop_data || {},
30678
+ parent_asset_id: variant.parent_asset_id || v2Response.data.asset_id,
30679
+ variant_type: variant.variant_type || "crop"
30680
+ };
30681
+ });
30682
+ return { result: legacyVariants };
30683
+ }
30684
+ function adaptUploadFromV2(v2Response) {
30685
+ if (!v2Response?.success || !v2Response?.data) {
30686
+ return { result: null };
30687
+ }
30688
+ const asset = v2Response.data;
30689
+ const legacyResponse = {
30690
+ id: asset.id,
30691
+ filename: asset.original_filename,
30692
+ mime_type: asset.mime_type,
30693
+ file_size: asset.file_size,
30694
+ width: asset.width,
30695
+ height: asset.height,
30696
+ status: asset.status,
30697
+ upload_date: asset.created_at,
30698
+ // URL del master
30699
+ url: makeAbsoluteUrl(asset.master?.url_signed),
30700
+ master_format: asset.master?.format,
30701
+ // Estado de procesamiento
30702
+ processing: asset.processing || {
30703
+ master_webp: asset.status === "processing" ? "queued" : "completed",
30704
+ variants: asset.status === "processing" ? "queued" : "completed"
30705
+ },
30706
+ // Información adicional
30707
+ checksum: asset.checksum,
30708
+ storage_path: asset.storage_path_base,
30709
+ metadata: asset.metadata || {}
30710
+ };
30711
+ return { result: legacyResponse };
30712
+ }
30713
+ function adaptVariantGenerationFromV2(v2Response) {
30714
+ if (!v2Response?.success || !v2Response?.data) {
30715
+ return { result: null };
30716
+ }
30717
+ const data = v2Response.data;
30718
+ const legacyResponse = {
30719
+ job_id: data.job_id,
30720
+ asset_id: data.asset_id,
30721
+ variants_requested: data.variants_requested || [],
30722
+ processing_mode: data.async ? "async" : "sync",
30723
+ estimated_completion: data.estimated_completion,
30724
+ // Estado de cada variante
30725
+ variant_statuses: (data.variant_statuses || []).map((status) => ({
30726
+ name: status.name,
30727
+ status: status.status,
30728
+ size: status.expected_size,
30729
+ format: status.format
30730
+ }))
30731
+ };
30732
+ return { result: legacyResponse };
30733
+ }
30734
+ function adaptErrorFromV2(error) {
30735
+ if (error.message && !error.message.includes("API Error:")) {
30736
+ return error;
30737
+ }
30738
+ let errorMessage = error.message || "Unknown API error";
30739
+ if (errorMessage.startsWith("API Error: ")) {
30740
+ errorMessage = errorMessage.substring(11);
30741
+ }
30742
+ return new Error(errorMessage);
30743
+ }
30744
+ const BASE_PATH$4 = "/api";
30745
+ async function listAssets(params = {}) {
30746
+ try {
30747
+ const queryString = new URLSearchParams(params).toString();
30748
+ const endpoint = queryString ? `/assets?${queryString}` : "/assets";
30749
+ const response = await callApi({
30750
+ endpoint,
30751
+ method: "GET",
30752
+ basePath: BASE_PATH$4,
30753
+ useJWT: true
30754
+ });
30755
+ return adaptAssetsListFromV2(response);
30756
+ } catch (error) {
30757
+ throw adaptErrorFromV2(error);
30758
+ }
30759
+ }
30760
+ async function uploadAsset(file, uploaded_by = null, store_original = false) {
30761
+ try {
30762
+ const formData = new FormData();
30763
+ formData.append("file", file);
30764
+ if (uploaded_by) formData.append("uploaded_by", uploaded_by);
30765
+ if (store_original) formData.append("store_original", store_original.toString());
30766
+ const response = await callApi({
30767
+ endpoint: "/assets",
30768
+ method: "POST",
30769
+ body: formData,
30770
+ basePath: BASE_PATH$4,
30771
+ isFormData: true,
30772
+ useJWT: true
30773
+ });
30774
+ return adaptUploadFromV2(response);
30775
+ } catch (error) {
30776
+ throw adaptErrorFromV2(error);
30777
+ }
30778
+ }
30779
+ async function deleteAsset(assetId) {
30780
+ try {
30781
+ const response = await callApi({
30782
+ endpoint: `/assets/${assetId}`,
30783
+ method: "DELETE",
30784
+ basePath: BASE_PATH$4,
30785
+ useJWT: true
30786
+ });
30787
+ return response;
30788
+ } catch (error) {
30789
+ throw adaptErrorFromV2(error);
30790
+ }
30791
+ }
30792
+ async function generateVariant(assetId, {
30793
+ variant_name,
30794
+ width,
30795
+ height,
30796
+ crop_params,
30797
+ preset_aspect = null,
30798
+ preset_size = null,
30799
+ output_format = "webp"
30800
+ }) {
30801
+ try {
30802
+ const variants = [{
30803
+ name: variant_name,
30804
+ width,
30805
+ height,
30806
+ output_format,
30807
+ crop_params
30808
+ }];
30809
+ if (preset_aspect) variants[0].preset_aspect = preset_aspect;
30810
+ if (preset_size) variants[0].preset_size = preset_size;
30811
+ const response = await callApi({
30812
+ endpoint: `/assets/${assetId}/variants`,
30813
+ method: "POST",
30814
+ body: {
30815
+ variants,
30816
+ async: false
30817
+ },
30818
+ basePath: BASE_PATH$4,
30819
+ useJWT: true
30820
+ });
30821
+ return adaptVariantGenerationFromV2(response);
30822
+ } catch (error) {
30823
+ throw adaptErrorFromV2(error);
30824
+ }
30825
+ }
30826
+ async function listVariants(assetId) {
30827
+ try {
30828
+ const response = await callApi({
30829
+ endpoint: `/assets/${assetId}/variants`,
30830
+ method: "GET",
30831
+ basePath: BASE_PATH$4,
30832
+ useJWT: true
30833
+ });
30834
+ const adaptedResponse = adaptVariantsListFromV2(response);
30835
+ return adaptedResponse;
30836
+ } catch (error) {
30837
+ throw adaptErrorFromV2(error);
30838
+ }
30839
+ }
30840
+ async function deleteVariant(assetId, variantId) {
30841
+ try {
30842
+ const response = await callApi({
30843
+ endpoint: `/assets/${assetId}/variants/${variantId}`,
30844
+ method: "DELETE",
30845
+ basePath: BASE_PATH$4,
30846
+ useJWT: true
30847
+ });
30848
+ return response;
30849
+ } catch (error) {
30850
+ throw adaptErrorFromV2(error);
30851
+ }
30852
+ }
30853
+ function useImageVariants() {
30854
+ const [variants, setVariants] = useState({});
30855
+ const [loading, setLoading] = useState({});
30856
+ const [errors, setErrors] = useState({});
30857
+ const loadVariants = useCallback(async (assetId) => {
30858
+ if (!assetId) return;
30859
+ if (loading[assetId]) return;
30860
+ if (variants[assetId]) return variants[assetId];
30861
+ setLoading((prev) => ({ ...prev, [assetId]: true }));
30862
+ setErrors((prev) => ({ ...prev, [assetId]: null }));
30863
+ try {
30864
+ const response = await listVariants(assetId);
30865
+ const variantsList = response?.result || [];
30866
+ setVariants((prev) => ({ ...prev, [assetId]: variantsList }));
30867
+ return variantsList;
30868
+ } catch (error) {
30869
+ console.error("Error loading variants:", error);
30870
+ setErrors((prev) => ({ ...prev, [assetId]: error.message }));
30871
+ return [];
30872
+ } finally {
30873
+ setLoading((prev) => ({ ...prev, [assetId]: false }));
30874
+ }
30875
+ }, [variants, loading]);
30876
+ const getVariants = useCallback((assetId) => {
30877
+ return variants[assetId] || [];
30878
+ }, [variants]);
30879
+ const isLoading = useCallback((assetId) => {
30880
+ return loading[assetId] || false;
30881
+ }, [loading]);
30882
+ const getError = useCallback((assetId) => {
30883
+ return errors[assetId] || null;
30884
+ }, [errors]);
30885
+ const clearVariants = useCallback((assetId) => {
30886
+ setVariants((prev) => {
30887
+ const newState = { ...prev };
30888
+ delete newState[assetId];
30889
+ return newState;
30890
+ });
30891
+ setLoading((prev) => {
30892
+ const newState = { ...prev };
30893
+ delete newState[assetId];
30894
+ return newState;
30895
+ });
30896
+ setErrors((prev) => {
30897
+ const newState = { ...prev };
30898
+ delete newState[assetId];
30899
+ return newState;
30900
+ });
30901
+ }, []);
30902
+ const refreshVariants = useCallback(async (assetId) => {
30903
+ clearVariants(assetId);
30904
+ return await loadVariants(assetId);
30905
+ }, [clearVariants, loadVariants]);
30906
+ const removeVariant = useCallback(async (assetId, variantId) => {
30907
+ try {
30908
+ await deleteVariant(assetId, variantId);
30909
+ setVariants((prev) => {
30910
+ const assetVariants = prev[assetId] || [];
30911
+ return {
30912
+ ...prev,
30913
+ [assetId]: assetVariants.filter((v) => v.id !== variantId)
30914
+ };
30915
+ });
30916
+ return { success: true };
30917
+ } catch (error) {
30918
+ console.error("Error deleting variant:", error);
30919
+ return { success: false, error: error.message };
30920
+ }
30921
+ }, []);
30922
+ return {
30923
+ loadVariants,
30924
+ getVariants,
30925
+ isLoading,
30926
+ getError,
30927
+ clearVariants,
30928
+ refreshVariants,
30929
+ removeVariant
30930
+ };
30931
+ }
30932
+ function ImageVariantsModal({
30933
+ image,
30934
+ isOpen,
30935
+ onClose,
30936
+ onSelect,
30937
+ onDelete,
30938
+ onCrop,
30939
+ onVariantDeleted,
30940
+ // Nueva prop para notificar cuando se elimina una variante
30941
+ allowedActions = {
30942
+ select: true,
30943
+ download: true,
30944
+ copy: true,
30945
+ delete: true,
30946
+ crop: true
30947
+ }
30948
+ }) {
30949
+ const { loadVariants, getVariants, isLoading, getError, removeVariant } = useImageVariants();
30950
+ const accessibilityManager = window.limboCore?.accessibilityManager;
30951
+ const [deleteMessage, setDeleteMessage] = React.useState(null);
30952
+ const [deleteMessageType, setDeleteMessageType] = React.useState(null);
30953
+ const config = window.limboCore?.config?.getGlobal() || {};
30954
+ const interaction = config.interaction || {
30955
+ allowSelection: false,
30956
+ allowCropping: true
30957
+ };
30958
+ const shouldShowSelectButton = interaction.allowSelection && allowedActions.select && onSelect;
30959
+ const shouldShowCropButton = interaction.allowCropping && allowedActions.crop && onCrop;
30960
+ const shouldShowCopyButton = allowedActions.copy;
30961
+ const shouldShowDownloadButton = allowedActions.download;
30962
+ const shouldShowDeleteButton = allowedActions.delete;
30963
+ const [failedVariants, setFailedVariants] = React.useState(/* @__PURE__ */ new Set());
30964
+ const apiVariants = React.useMemo(
30965
+ () => image?.variants || [],
30966
+ [image?.variants]
30967
+ );
30968
+ const hookVariants = getVariants(image?.id);
30969
+ const variants = apiVariants.length > 0 ? apiVariants : hookVariants;
30970
+ const loading = apiVariants.length === 0 ? isLoading(image?.id) : false;
30971
+ const error = apiVariants.length === 0 ? getError(image?.id) : null;
30972
+ useEffect(() => {
30973
+ if (isOpen && image?.id && (!image?.variants || image.variants.length === 0)) {
30974
+ loadVariants(image.id);
30975
+ }
30976
+ }, [isOpen, image?.id, image?.variants, loadVariants]);
30977
+ React.useEffect(() => {
30978
+ if (isOpen) {
30979
+ setFailedVariants(/* @__PURE__ */ new Set());
30980
+ }
30981
+ }, [isOpen]);
30982
+ const handleVariantImageError = (variantId) => {
30983
+ setFailedVariants((prev) => /* @__PURE__ */ new Set([...prev, variantId]));
30984
+ };
30985
+ if (!isOpen) return null;
30986
+ const handleSelectVariant = (variant) => {
30987
+ const variantAsImage = {
30988
+ id: variant.id,
30989
+ filename: `${image.filename}_${variant.name}`,
30990
+ url: variant.url,
30991
+ width: variant.width,
30992
+ height: variant.height,
30993
+ mime_type: `image/${variant.format}`,
30994
+ file_size: variant.file_size,
30995
+ upload_date: variant.created_at,
30996
+ processing_status: "completed",
30997
+ is_variant: true,
30998
+ parent_asset_id: image.id,
30999
+ variant_info: variant
31000
+ };
31001
+ accessibilityManager?.announce(`Variante seleccionada: ${variant.name}`);
31002
+ onSelect?.(variantAsImage);
31003
+ onClose?.();
31004
+ };
31005
+ const handleCopyVariantUrl = async (variant) => {
31006
+ try {
31007
+ await navigator.clipboard.writeText(variant.url);
31008
+ accessibilityManager?.announce(`URL de variante ${variant.name} copiada`);
31009
+ } catch (err) {
31010
+ console.error("Error copying variant URL:", err);
31011
+ accessibilityManager?.announceError("Error al copiar URL de variante");
31012
+ }
31013
+ };
31014
+ const handleDownloadVariant = (variant) => {
31015
+ accessibilityManager?.announce(`Descargando variante ${variant.name}`);
31016
+ const a = document.createElement("a");
31017
+ a.href = variant.url;
31018
+ a.download = `${image.filename}_${variant.name}.${variant.format}`;
31019
+ document.body.appendChild(a);
31020
+ a.click();
31021
+ document.body.removeChild(a);
31022
+ };
31023
+ const handleViewVariant = (variant) => {
31024
+ accessibilityManager?.announce(
31025
+ `Abriendo variante ${variant.name} en nueva pestaña`
31026
+ );
31027
+ window.open(variant.url, "_blank");
31028
+ };
31029
+ const handleViewOriginal = () => {
31030
+ accessibilityManager?.announce(
31031
+ `Abriendo imagen original ${image.filename} en nueva pestaña`
31032
+ );
31033
+ window.open(image.url, "_blank");
31034
+ };
31035
+ const handleCopyOriginalUrl = async () => {
31036
+ try {
31037
+ await navigator.clipboard.writeText(image.url);
31038
+ accessibilityManager?.announce(`URL de imagen original copiada`);
31039
+ } catch (err) {
31040
+ console.error("Error copying original URL:", err);
31041
+ accessibilityManager?.announceError("Error al copiar URL");
31042
+ }
31043
+ };
31044
+ const handleDownloadOriginal = () => {
31045
+ accessibilityManager?.announce(
31046
+ `Descargando imagen original ${image.filename}`
31047
+ );
31048
+ const a = document.createElement("a");
31049
+ a.href = image.url;
31050
+ a.download = image.filename;
31051
+ document.body.appendChild(a);
31052
+ a.click();
31053
+ document.body.removeChild(a);
31054
+ };
31055
+ const handleDeleteOriginal = async () => {
31056
+ if (!onDelete) return;
31057
+ const confirmed = window.confirm(
31058
+ `¿Estás seguro de que deseas eliminar "${image.filename}"? Esta acción también eliminará todas sus variantes.`
31059
+ );
31060
+ if (confirmed) {
31061
+ accessibilityManager?.announce(`Eliminando imagen ${image.filename}`);
31062
+ await onDelete(image.id);
31063
+ onClose?.();
31064
+ }
31065
+ };
31066
+ const handleCropOriginal = () => {
31067
+ if (!onCrop) return;
31068
+ accessibilityManager?.announce(
31069
+ `Abriendo herramienta de recorte para ${image.filename}`
31070
+ );
31071
+ onCrop(image);
31072
+ onClose?.();
31073
+ };
31074
+ const handleDeleteVariant = async (variant) => {
31075
+ const confirmed = window.confirm(
31076
+ `¿Estás seguro de que deseas eliminar la variante "${variant.name || variant.filename}"?`
31077
+ );
31078
+ if (confirmed) {
31079
+ accessibilityManager?.announce(
31080
+ `Eliminando variante ${variant.name || variant.filename}`
31081
+ );
31082
+ const result = await removeVariant(image.id, variant.id);
31083
+ if (result.success) {
31084
+ setDeleteMessage("Variante eliminada correctamente");
31085
+ setDeleteMessageType("success");
31086
+ accessibilityManager?.announce(`Variante eliminada correctamente`);
31087
+ onVariantDeleted?.();
31088
+ setTimeout(() => {
31089
+ setDeleteMessage(null);
31090
+ setDeleteMessageType(null);
31091
+ }, 3e3);
31092
+ } else {
31093
+ setDeleteMessage(`Error al eliminar variante: ${result.error}`);
31094
+ setDeleteMessageType("error");
31095
+ accessibilityManager?.announceError(
31096
+ `Error al eliminar variante: ${result.error}`
31097
+ );
31098
+ setTimeout(() => {
31099
+ setDeleteMessage(null);
31100
+ setDeleteMessageType(null);
31101
+ }, 5e3);
31102
+ }
31103
+ }
31104
+ };
31105
+ return /* @__PURE__ */ jsx(
31106
+ "div",
31107
+ {
31108
+ className: "limbo-modal-overlay",
31109
+ onClick: onClose,
31110
+ role: "dialog",
31111
+ "aria-modal": "true",
31112
+ "aria-labelledby": "variants-modal-title",
31113
+ children: /* @__PURE__ */ jsxs(
31114
+ "div",
31115
+ {
31116
+ className: "limbo-modal-content limbo-variants-modal",
31117
+ onClick: (e) => e.stopPropagation(),
31118
+ children: [
31119
+ /* @__PURE__ */ jsxs("div", { className: "limbo-modal-header", children: [
31120
+ /* @__PURE__ */ jsxs("h2", { id: "variants-modal-title", children: [
31121
+ "Variantes de ",
31122
+ image?.filename
31123
+ ] }),
31124
+ /* @__PURE__ */ jsx(
31125
+ "button",
31126
+ {
31127
+ className: "limbo-modal-close",
31128
+ onClick: onClose,
31129
+ "aria-label": "Cerrar modal de variantes",
31130
+ children: "✕"
31131
+ }
31132
+ )
31133
+ ] }),
31134
+ deleteMessage && /* @__PURE__ */ jsx(
31135
+ "div",
31136
+ {
31137
+ className: `limbo-variants-message limbo-variants-message--${deleteMessageType} mt-1`,
31138
+ role: "alert",
31139
+ style: {
31140
+ padding: "12px 20px",
31141
+ margin: "0 20px 15px 20px",
31142
+ borderRadius: "4px",
31143
+ backgroundColor: deleteMessageType === "success" ? "#d4edda" : "#f8d7da",
31144
+ color: deleteMessageType === "success" ? "#155724" : "#721c24",
31145
+ border: `1px solid ${deleteMessageType === "success" ? "#c3e6cb" : "#f5c6cb"}`
31146
+ },
31147
+ children: deleteMessage
31148
+ }
31149
+ ),
31150
+ /* @__PURE__ */ jsx("div", { className: "limbo-modal-body", children: loading ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-loading", children: [
31151
+ /* @__PURE__ */ jsx("div", { className: "limbo-loader" }),
31152
+ /* @__PURE__ */ jsx("p", { children: "Cargando variantes..." })
31153
+ ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-error", children: [
31154
+ /* @__PURE__ */ jsxs("p", { children: [
31155
+ "Error al cargar variantes: ",
31156
+ error
31157
+ ] }),
31158
+ /* @__PURE__ */ jsx(
31159
+ "button",
31160
+ {
31161
+ onClick: () => loadVariants(image.id),
31162
+ className: "btn btn-primary",
31163
+ children: "Reintentar"
31164
+ }
31165
+ )
31166
+ ] }) : variants.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "limbo-variants-empty", children: [
31167
+ /* @__PURE__ */ jsx("p", { children: "Esta imagen no tiene variantes aún." }),
31168
+ /* @__PURE__ */ jsx("small", { children: "Las variantes aparecerán aquí después de hacer recortes o redimensionados." })
31169
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
31170
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-section", children: [
31171
+ /* @__PURE__ */ jsx("h3", { children: "Imagen original" }),
31172
+ /* @__PURE__ */ jsxs(
31173
+ "div",
31174
+ {
31175
+ onClick: handleViewOriginal,
31176
+ className: "limbo-variant-card limbo-variant-original cursor-pointer",
31177
+ children: [
31178
+ /* @__PURE__ */ jsx("div", { className: "limbo-variant-preview", children: /* @__PURE__ */ jsx("img", { src: image.url, alt: image.filename, loading: "lazy" }) }),
31179
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-info", children: [
31180
+ /* @__PURE__ */ jsx("div", { className: "limbo-variant-name", children: image.filename }),
31181
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-meta", children: [
31182
+ /* @__PURE__ */ jsxs("span", { children: [
31183
+ image.width,
31184
+ "×",
31185
+ image.height
31186
+ ] }),
31187
+ /* @__PURE__ */ jsxs("span", { children: [
31188
+ Math.round(image.file_size / 1024),
31189
+ " KB"
31190
+ ] })
31191
+ ] })
31192
+ ] }),
31193
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-actions", children: [
31194
+ shouldShowSelectButton && /* @__PURE__ */ jsx(
31195
+ "button",
31196
+ {
31197
+ className: "btn btn-success btn-sm",
31198
+ onClick: (e) => {
31199
+ e.stopPropagation();
31200
+ onSelect?.(image);
31201
+ onClose?.();
31202
+ },
31203
+ title: "Seleccionar original",
31204
+ children: "Seleccionar"
31205
+ }
31206
+ ),
31207
+ shouldShowCropButton && /* @__PURE__ */ jsx(
31208
+ "button",
31209
+ {
31210
+ className: "btn btn-crop btn-sm",
31211
+ onClick: (e) => {
31212
+ e.stopPropagation();
31213
+ handleCropOriginal();
31214
+ },
31215
+ title: "Recortar imagen",
31216
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-scissors-white icon--sm" })
31217
+ }
31218
+ ),
31219
+ shouldShowCopyButton && /* @__PURE__ */ jsx(
31220
+ "button",
31221
+ {
31222
+ className: "btn btn-secondary btn-sm",
31223
+ onClick: (e) => {
31224
+ e.stopPropagation();
31225
+ handleCopyOriginalUrl();
31226
+ },
31227
+ title: "Copiar URL",
31228
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-copy-white icon--sm" })
31229
+ }
31230
+ ),
31231
+ shouldShowDownloadButton && /* @__PURE__ */ jsx(
31232
+ "button",
31233
+ {
31234
+ className: "btn btn-primary btn-sm",
31235
+ onClick: (e) => {
31236
+ e.stopPropagation();
31237
+ handleDownloadOriginal();
31238
+ },
31239
+ title: "Descargar",
31240
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-download-white icon--sm" })
31241
+ }
31242
+ ),
31243
+ shouldShowDeleteButton && onDelete && /* @__PURE__ */ jsx(
31244
+ "button",
31245
+ {
31246
+ className: "btn btn-danger btn-sm",
31247
+ onClick: (e) => {
31248
+ e.stopPropagation();
31249
+ handleDeleteOriginal();
31250
+ },
31251
+ title: "Eliminar imagen",
31252
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-close-small-white icon--sm m-[0!important]" })
31253
+ }
31254
+ )
31255
+ ] })
31256
+ ]
31257
+ }
31258
+ )
31259
+ ] }),
31260
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-section", children: [
31261
+ /* @__PURE__ */ jsxs("h3", { children: [
31262
+ "Variantes (",
31263
+ variants.length,
31264
+ ")"
31265
+ ] }),
31266
+ /* @__PURE__ */ jsx("div", { className: "limbo-variants-grid", children: variants.map((variant) => {
31267
+ const hasImageError = failedVariants.has(variant.id);
31268
+ return /* @__PURE__ */ jsxs(
31269
+ "div",
31270
+ {
31271
+ className: `limbo-variant-card ${!hasImageError ? "cursor-pointer" : ""}`,
31272
+ onClick: !hasImageError ? () => handleViewVariant(variant) : void 0,
31273
+ children: [
31274
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-preview", children: [
31275
+ /* @__PURE__ */ jsx(
31276
+ "img",
31277
+ {
31278
+ src: variant.url,
31279
+ alt: variant.name || variant.filename || `Variante ${variant.id}`,
31280
+ loading: "lazy",
31281
+ onError: () => handleVariantImageError(variant.id),
31282
+ style: {
31283
+ display: hasImageError ? "none" : "block"
31284
+ }
31285
+ }
31286
+ ),
31287
+ hasImageError && /* @__PURE__ */ jsx(
31288
+ "div",
31289
+ {
31290
+ className: "limbo-variant-error",
31291
+ style: {
31292
+ display: "flex",
31293
+ alignItems: "center",
31294
+ justifyContent: "center",
31295
+ height: "100px",
31296
+ backgroundColor: "#f5f5f5",
31297
+ color: "#666"
31298
+ },
31299
+ children: /* @__PURE__ */ jsx("span", { children: "Error al cargar preview" })
31300
+ }
31301
+ )
31302
+ ] }),
31303
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-info", children: [
31304
+ /* @__PURE__ */ jsx("div", { className: "limbo-variant-name", children: variant.name || variant.filename || `Variante ${variant.id}` }),
31305
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-meta", children: [
31306
+ /* @__PURE__ */ jsxs("span", { children: [
31307
+ variant.width,
31308
+ "×",
31309
+ variant.height
31310
+ ] }),
31311
+ /* @__PURE__ */ jsx("span", { children: (variant.format || "jpg").toUpperCase() }),
31312
+ /* @__PURE__ */ jsxs("span", { children: [
31313
+ Math.round((variant.file_size || 0) / 1024),
31314
+ " KB"
31315
+ ] })
31316
+ ] }),
31317
+ variant.crop_params && /* @__PURE__ */ jsxs("div", { className: "limbo-variant-crop-badge", children: [
31318
+ /* @__PURE__ */ jsx("span", { className: "icon icon-scissors icon--xs" }),
31319
+ " ",
31320
+ "Recortada"
31321
+ ] })
31322
+ ] }),
31323
+ /* @__PURE__ */ jsxs("div", { className: "limbo-variant-actions", children: [
31324
+ shouldShowSelectButton && /* @__PURE__ */ jsx(
31325
+ "button",
31326
+ {
31327
+ className: "btn btn-success btn-sm",
31328
+ onClick: (e) => {
31329
+ e.stopPropagation();
31330
+ handleSelectVariant(variant);
31331
+ },
31332
+ title: `Seleccionar variante ${variant.name || variant.filename || variant.id}`,
31333
+ children: "Seleccionar"
31334
+ }
31335
+ ),
31336
+ shouldShowCopyButton && /* @__PURE__ */ jsx(
31337
+ "button",
31338
+ {
31339
+ className: "btn btn-secondary btn-sm",
31340
+ onClick: (e) => {
31341
+ e.stopPropagation();
31342
+ handleCopyVariantUrl(variant);
31343
+ },
31344
+ title: "Copiar URL",
31345
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-copy-white icon--sm" })
31346
+ }
31347
+ ),
31348
+ shouldShowDownloadButton && /* @__PURE__ */ jsx(
31349
+ "button",
31350
+ {
31351
+ className: "btn btn-primary btn-sm",
31352
+ onClick: (e) => {
31353
+ e.stopPropagation();
31354
+ handleDownloadVariant(variant);
31355
+ },
31356
+ title: "Descargar",
31357
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-download-white icon--sm" })
31358
+ }
31359
+ ),
31360
+ shouldShowDeleteButton && /* @__PURE__ */ jsx(
31361
+ "button",
31362
+ {
31363
+ className: "btn btn-danger btn-sm",
31364
+ onClick: (e) => {
31365
+ e.stopPropagation();
31366
+ handleDeleteVariant(variant);
31367
+ },
31368
+ title: "Eliminar variante",
31369
+ children: /* @__PURE__ */ jsx("span", { className: "icon icon-close-small-white icon--sm m-[0!important]" })
31370
+ }
31371
+ )
31372
+ ] })
31373
+ ]
31374
+ },
31375
+ variant.id
31376
+ );
31377
+ }) })
31378
+ ] })
31379
+ ] }) })
31380
+ ]
31381
+ }
31382
+ )
31383
+ }
31384
+ );
31385
+ }
30455
31386
  function ImageCard({
30456
31387
  image,
30457
31388
  onDelete,
@@ -30465,10 +31396,14 @@ function ImageCard({
30465
31396
  download: true,
30466
31397
  copy: true,
30467
31398
  delete: true,
30468
- crop: true
31399
+ crop: true,
31400
+ variants: true
31401
+ // Nueva acción para ver variantes
30469
31402
  }
30470
31403
  }) {
30471
31404
  const [copied, setCopied] = useState(false);
31405
+ const [showVariants, setShowVariants] = useState(false);
31406
+ const [localVariantsCount, setLocalVariantsCount] = useState(image.variants_count || 0);
30472
31407
  const { isMobile, isTouch } = useMobileDetection();
30473
31408
  const accessibilityManager = window.limboCore?.accessibilityManager;
30474
31409
  const config = window.limboCore?.config?.getGlobal() || {};
@@ -30484,21 +31419,41 @@ function ImageCard({
30484
31419
  );
30485
31420
  onDelete?.(image);
30486
31421
  };
30487
- const handleCopyUrl = (e) => {
31422
+ const handleCopyUrl = async (e) => {
30488
31423
  e.stopPropagation();
30489
- navigator.clipboard.writeText(image.path).then(() => {
31424
+ let textToCopy = image.url || image.path;
31425
+ try {
31426
+ if (navigator.clipboard && window.isSecureContext) {
31427
+ await navigator.clipboard.writeText(textToCopy);
31428
+ } else {
31429
+ const textArea = document.createElement("textarea");
31430
+ textArea.value = textToCopy;
31431
+ textArea.style.position = "fixed";
31432
+ textArea.style.opacity = "0";
31433
+ document.body.appendChild(textArea);
31434
+ textArea.focus();
31435
+ textArea.select();
31436
+ document.execCommand("copy");
31437
+ document.body.removeChild(textArea);
31438
+ }
30490
31439
  setCopied(true);
30491
31440
  accessibilityManager?.announce(
30492
- `URL de ${image.filename} copiada al portapapeles`
31441
+ `URL de ${image.filename} copiada al portapapeles. Nota: La URL puede tener un tiempo de validez limitado.`
30493
31442
  );
30494
31443
  setTimeout(() => setCopied(false), 2e3);
30495
- });
31444
+ } catch (err) {
31445
+ console.error("Error al copiar URL:", err);
31446
+ accessibilityManager?.announce(
31447
+ `Error al copiar URL de ${image.filename}. Inténtalo de nuevo.`
31448
+ );
31449
+ alert(`Error al copiar URL de ${image.filename}. Por favor, inténtalo de nuevo.`);
31450
+ }
30496
31451
  };
30497
31452
  const handleDownload = (e) => {
30498
31453
  e.preventDefault();
30499
31454
  e.stopPropagation();
30500
31455
  accessibilityManager?.announce(`Descargando ${image.filename}`);
30501
- fetch(image.path, { mode: "cors" }).then((resp) => resp.blob()).then((blob) => {
31456
+ fetch(image.url || image.path, { mode: "cors" }).then((resp) => resp.blob()).then((blob) => {
30502
31457
  const url = window.URL.createObjectURL(blob);
30503
31458
  const a = document.createElement("a");
30504
31459
  a.href = url;
@@ -30513,9 +31468,22 @@ function ImageCard({
30513
31468
  };
30514
31469
  const handleCrop = (e) => {
30515
31470
  e.stopPropagation();
30516
- accessibilityManager?.announce(`Abriendo editor para ${image.filename}`);
31471
+ accessibilityManager?.announce(`Editando imagen ${image.filename}`);
30517
31472
  onCrop?.(image);
30518
31473
  };
31474
+ const hasVariants = image.variants && image.variants.length > 0 || localVariantsCount > 0;
31475
+ const variantsCount = localVariantsCount;
31476
+ const handleVariantDeleted = React.useCallback(() => {
31477
+ setLocalVariantsCount((prev) => Math.max(0, prev - 1));
31478
+ }, []);
31479
+ React.useEffect(() => {
31480
+ setLocalVariantsCount(image.variants_count || 0);
31481
+ }, [image.variants_count]);
31482
+ const handleShowVariants = (e) => {
31483
+ e.stopPropagation();
31484
+ accessibilityManager?.announce(`Mostrando variantes de ${image.filename}`);
31485
+ setShowVariants(true);
31486
+ };
30519
31487
  const handleSelect = (e) => {
30520
31488
  e.stopPropagation();
30521
31489
  accessibilityManager?.announce(
@@ -30525,6 +31493,7 @@ function ImageCard({
30525
31493
  };
30526
31494
  const shouldShowSelectButton = interaction.allowSelection && allowedActions.select && onSelect;
30527
31495
  const shouldShowCropButton = interaction.allowCropping && allowedActions.crop && onCrop;
31496
+ const shouldShowVariantsButton = hasVariants && allowedActions.variants !== false;
30528
31497
  const handleKeyDown = (e) => {
30529
31498
  switch (e.key) {
30530
31499
  case "Enter":
@@ -30560,194 +31529,274 @@ function ImageCard({
30560
31529
  handleCrop(e);
30561
31530
  }
30562
31531
  break;
31532
+ case "v":
31533
+ case "V":
31534
+ if (allowedActions.variants) {
31535
+ e.preventDefault();
31536
+ handleShowVariants(e);
31537
+ }
31538
+ break;
30563
31539
  }
30564
31540
  };
30565
31541
  const handleImageClick = () => {
30566
- window.open(image.path, "_blank");
31542
+ window.open(image.url || image.path, "_blank");
30567
31543
  };
30568
- return /* @__PURE__ */ jsxs(
30569
- "div",
30570
- {
30571
- className: `limbo-image-card flex flex-col items-center cursor-pointer relative transition ${isDeleting ? "opacity-50" : ""} ${isMobile ? "limbo-image-card--mobile" : ""}`,
30572
- onClick: handleImageClick,
30573
- onKeyDown: handleKeyDown,
30574
- title: isMobile ? "Toque para ver imagen" : "Haga click en la imagen para ver preview en nueva pestaña (Enter/Space para abrir, D descargar, C copiar, Delete eliminar, X recortar)",
30575
- role: "button",
30576
- tabIndex: 0,
30577
- "aria-label": `Imagen ${image.filename}. ${image.width}x${image.height} px`,
30578
- style: {
30579
- // Optimización para touch
30580
- ...isTouch && {
30581
- touchAction: "manipulation",
30582
- WebkitTapHighlightColor: "transparent"
30583
- }
30584
- },
30585
- children: [
30586
- /* @__PURE__ */ jsxs(
30587
- "div",
30588
- {
30589
- className: `limbo-image-actions ${isMobile ? "limbo-image-actions--mobile" : ""}`,
30590
- children: [
30591
- allowedActions.copy && /* @__PURE__ */ jsx(
30592
- "button",
30593
- {
30594
- type: "button",
30595
- title: copied ? "¡Copiado!" : "Copiar URL",
30596
- className: `btn btn-copy border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
30597
- onClick: handleCopyUrl,
30598
- tabIndex: -1,
30599
- style: {
30600
- ...isMobile && {
30601
- width: "40px",
30602
- height: "40px"
30603
- }
30604
- },
30605
- children: /* @__PURE__ */ jsx(
30606
- "span",
30607
- {
30608
- className: `icon ${copied ? "icon-copied-white" : "icon-copy-white"} icon--sm`,
30609
- "aria-hidden": "true"
30610
- }
30611
- )
30612
- }
30613
- ),
30614
- shouldShowSelectButton && /* @__PURE__ */ jsx(
30615
- "button",
30616
- {
30617
- type: "button",
30618
- title: "Seleccionar imagen",
30619
- className: `btn btn-success border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
30620
- onClick: handleSelect,
30621
- tabIndex: -1,
30622
- style: {
30623
- ...isMobile && {
30624
- width: "40px",
30625
- height: "40px"
30626
- }
30627
- },
30628
- children: /* @__PURE__ */ jsx(
30629
- "span",
30630
- {
30631
- className: "icon icon-tick-white icon--sm",
30632
- "aria-hidden": "true"
30633
- }
30634
- )
30635
- }
30636
- ),
30637
- shouldShowCropButton && /* @__PURE__ */ jsx(
30638
- "button",
30639
- {
30640
- type: "button",
30641
- title: "Editar imagen",
30642
- className: `btn btn-crop border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
30643
- onClick: handleCrop,
30644
- tabIndex: -1,
30645
- style: {
30646
- ...isMobile && {
30647
- width: "40px",
30648
- height: "40px"
30649
- }
30650
- },
30651
- children: /* @__PURE__ */ jsx(
30652
- "span",
30653
- {
30654
- className: "icon icon-scissors-white icon--sm",
30655
- "aria-hidden": "true"
30656
- }
30657
- )
30658
- }
30659
- ),
30660
- allowedActions.download && /* @__PURE__ */ jsx(
30661
- "button",
30662
- {
30663
- type: "button",
30664
- title: "Descargar",
30665
- className: `btn btn-download border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
30666
- onClick: handleDownload,
30667
- tabIndex: -1,
30668
- style: {
30669
- ...isMobile && {
30670
- width: "40px",
30671
- height: "40px"
30672
- }
30673
- },
30674
- children: /* @__PURE__ */ jsx(
30675
- "span",
30676
- {
30677
- className: "icon icon-download-white icon--sm",
30678
- "aria-hidden": "true"
30679
- }
30680
- )
30681
- }
30682
- ),
30683
- allowedActions.delete && onDelete && /* @__PURE__ */ jsx(
30684
- "button",
30685
- {
30686
- onClick: handleDelete,
30687
- disabled: isDeleting,
30688
- className: `btn btn-delete border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
30689
- title: "Eliminar imagen",
30690
- tabIndex: -1,
30691
- style: {
30692
- ...isMobile && {
30693
- width: "40px",
30694
- height: "40px"
30695
- }
30696
- },
30697
- children: isDeleting ? "…" : /* @__PURE__ */ jsx(
30698
- "span",
30699
- {
30700
- className: "icon icon-close-small-white icon--sm",
30701
- "aria-hidden": "true"
30702
- }
30703
- )
30704
- }
30705
- )
30706
- ]
31544
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
31545
+ /* @__PURE__ */ jsxs(
31546
+ "div",
31547
+ {
31548
+ className: `limbo-image-card flex flex-col items-center cursor-pointer relative transition ${isDeleting ? "opacity-50" : ""} ${isMobile ? "limbo-image-card--mobile" : ""}`,
31549
+ onClick: handleImageClick,
31550
+ onKeyDown: handleKeyDown,
31551
+ title: isMobile ? "Toque para ver imagen" : "Haga click en la imagen para ver preview en nueva pestaña (Enter/Space para abrir, D descargar, C copiar, Delete eliminar, X recortar)",
31552
+ role: "button",
31553
+ tabIndex: 0,
31554
+ "aria-label": `Imagen ${image.filename}. ${image.width}x${image.height} px`,
31555
+ style: {
31556
+ // Optimización para touch
31557
+ ...isTouch && {
31558
+ touchAction: "manipulation",
31559
+ WebkitTapHighlightColor: "transparent"
30707
31560
  }
30708
- ),
30709
- /* @__PURE__ */ jsx(
30710
- "img",
30711
- {
30712
- src: image.path,
30713
- alt: image.filename,
30714
- className: "w-full object-cover rounded aspect-square",
30715
- sizes: `height: ${thumbnailSize * 6}px,width: ${thumbnailSize * 6}px`,
30716
- draggable: false,
30717
- style: {
30718
- // Better responsive behavior for images
30719
- ...isMobile && {
30720
- minHeight: `${Math.max(thumbnailSize * 3, 80)}px`,
30721
- height: "auto",
30722
- aspectRatio: "1 / 1",
30723
- objectFit: "cover"
30724
- }
31561
+ },
31562
+ children: [
31563
+ /* @__PURE__ */ jsxs(
31564
+ "div",
31565
+ {
31566
+ className: `limbo-image-actions ${isMobile ? "limbo-image-actions--mobile" : ""}`,
31567
+ children: [
31568
+ allowedActions.copy && /* @__PURE__ */ jsx(
31569
+ "button",
31570
+ {
31571
+ type: "button",
31572
+ title: copied ? "¡Copiado!" : "Copiar URL",
31573
+ className: `btn btn-copy border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31574
+ onClick: handleCopyUrl,
31575
+ tabIndex: -1,
31576
+ style: {
31577
+ ...isMobile && {
31578
+ width: "40px",
31579
+ height: "40px"
31580
+ }
31581
+ },
31582
+ children: /* @__PURE__ */ jsx(
31583
+ "span",
31584
+ {
31585
+ className: `icon ${copied ? "icon-copied-white" : "icon-copy-white"} icon--sm`,
31586
+ "aria-hidden": "true"
31587
+ }
31588
+ )
31589
+ }
31590
+ ),
31591
+ shouldShowSelectButton && /* @__PURE__ */ jsx(
31592
+ "button",
31593
+ {
31594
+ type: "button",
31595
+ title: "Seleccionar imagen",
31596
+ className: `btn btn-success border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31597
+ onClick: handleSelect,
31598
+ tabIndex: -1,
31599
+ style: {
31600
+ ...isMobile && {
31601
+ width: "40px",
31602
+ height: "40px"
31603
+ }
31604
+ },
31605
+ children: /* @__PURE__ */ jsx(
31606
+ "span",
31607
+ {
31608
+ className: "icon icon-tick-white icon--sm",
31609
+ "aria-hidden": "true"
31610
+ }
31611
+ )
31612
+ }
31613
+ ),
31614
+ shouldShowCropButton && /* @__PURE__ */ jsx(
31615
+ "button",
31616
+ {
31617
+ type: "button",
31618
+ title: "Editar imagen",
31619
+ className: `btn btn-crop border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31620
+ onClick: handleCrop,
31621
+ tabIndex: -1,
31622
+ style: {
31623
+ ...isMobile && {
31624
+ width: "40px",
31625
+ height: "40px"
31626
+ }
31627
+ },
31628
+ children: /* @__PURE__ */ jsx(
31629
+ "span",
31630
+ {
31631
+ className: "icon icon-scissors-white icon--sm",
31632
+ "aria-hidden": "true"
31633
+ }
31634
+ )
31635
+ }
31636
+ ),
31637
+ shouldShowVariantsButton && /* @__PURE__ */ jsxs(
31638
+ "button",
31639
+ {
31640
+ type: "button",
31641
+ title: `Ver ${variantsCount} variante${variantsCount !== 1 ? "s" : ""}`,
31642
+ className: `btn btn-variants border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31643
+ onClick: handleShowVariants,
31644
+ tabIndex: -1,
31645
+ style: {
31646
+ ...isMobile && {
31647
+ width: "40px",
31648
+ height: "40px"
31649
+ },
31650
+ position: "relative"
31651
+ },
31652
+ children: [
31653
+ /* @__PURE__ */ jsx(
31654
+ "span",
31655
+ {
31656
+ className: "icon icon-album-menu-white icon--sm",
31657
+ "aria-hidden": "true"
31658
+ }
31659
+ ),
31660
+ variantsCount > 0 && /* @__PURE__ */ jsx(
31661
+ "span",
31662
+ {
31663
+ className: "variants-count-badge",
31664
+ style: {
31665
+ position: "absolute",
31666
+ top: "-8px",
31667
+ right: "-8px",
31668
+ background: "#dc2626",
31669
+ color: "white",
31670
+ borderRadius: "50%",
31671
+ minWidth: "20px",
31672
+ height: "20px",
31673
+ fontSize: "12px",
31674
+ fontWeight: "700",
31675
+ display: "flex",
31676
+ alignItems: "center",
31677
+ justifyContent: "center",
31678
+ lineHeight: "1",
31679
+ border: "2px solid white",
31680
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.4)",
31681
+ zIndex: "999"
31682
+ },
31683
+ children: variantsCount
31684
+ }
31685
+ )
31686
+ ]
31687
+ }
31688
+ ),
31689
+ allowedActions.download && /* @__PURE__ */ jsx(
31690
+ "button",
31691
+ {
31692
+ type: "button",
31693
+ title: "Descargar",
31694
+ className: `btn btn-download border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31695
+ onClick: handleDownload,
31696
+ tabIndex: -1,
31697
+ style: {
31698
+ ...isMobile && {
31699
+ width: "40px",
31700
+ height: "40px"
31701
+ }
31702
+ },
31703
+ children: /* @__PURE__ */ jsx(
31704
+ "span",
31705
+ {
31706
+ className: "icon icon-download-white icon--sm",
31707
+ "aria-hidden": "true"
31708
+ }
31709
+ )
31710
+ }
31711
+ ),
31712
+ allowedActions.delete && onDelete && /* @__PURE__ */ jsx(
31713
+ "button",
31714
+ {
31715
+ onClick: handleDelete,
31716
+ disabled: isDeleting,
31717
+ className: `btn btn-delete border border-brand-blue-050/50 ${isMobile ? "btn--touch" : ""}`,
31718
+ title: "Eliminar imagen",
31719
+ tabIndex: -1,
31720
+ style: {
31721
+ ...isMobile && {
31722
+ width: "40px",
31723
+ height: "40px"
31724
+ }
31725
+ },
31726
+ children: isDeleting ? "…" : /* @__PURE__ */ jsx(
31727
+ "span",
31728
+ {
31729
+ className: "icon icon-close-small-white icon--sm",
31730
+ "aria-hidden": "true"
31731
+ }
31732
+ )
31733
+ }
31734
+ )
31735
+ ]
30725
31736
  }
30726
- }
30727
- ),
30728
- /* @__PURE__ */ jsx(
30729
- "span",
30730
- {
30731
- className: `text-xs mt-1 truncate w-full text-center limbo-image-card-name ${isMobile ? "limbo-image-card-name--mobile" : ""}`,
30732
- style: {
30733
- // Better text visibility on mobile
30734
- ...isMobile && {
30735
- fontSize: "0.75rem",
30736
- lineHeight: "1.2",
30737
- maxWidth: "100%",
30738
- padding: "2px 4px",
30739
- backgroundColor: "rgba(255, 255, 255, 0.9)",
30740
- borderRadius: "4px",
30741
- color: "#333",
30742
- fontWeight: "500"
31737
+ ),
31738
+ /* @__PURE__ */ jsx(
31739
+ "img",
31740
+ {
31741
+ src: image.url || image.path,
31742
+ alt: image.filename,
31743
+ className: "w-full object-cover rounded aspect-square",
31744
+ sizes: `height: ${thumbnailSize * 6}px,width: ${thumbnailSize * 6}px`,
31745
+ draggable: false,
31746
+ style: {
31747
+ // Better responsive behavior for images
31748
+ ...isMobile && {
31749
+ minHeight: `${Math.max(thumbnailSize * 3, 80)}px`,
31750
+ height: "auto",
31751
+ aspectRatio: "1 / 1",
31752
+ objectFit: "cover"
31753
+ }
30743
31754
  }
30744
- },
30745
- children: image.filename
30746
- }
30747
- )
30748
- ]
30749
- }
30750
- );
31755
+ }
31756
+ ),
31757
+ /* @__PURE__ */ jsx(
31758
+ "span",
31759
+ {
31760
+ className: `text-xs mt-1 truncate w-full text-center limbo-image-card-name ${isMobile ? "limbo-image-card-name--mobile" : ""}`,
31761
+ style: {
31762
+ // Better text visibility on mobile
31763
+ ...isMobile && {
31764
+ fontSize: "0.75rem",
31765
+ lineHeight: "1.2",
31766
+ maxWidth: "100%",
31767
+ padding: "2px 4px",
31768
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
31769
+ borderRadius: "4px",
31770
+ color: "#333",
31771
+ fontWeight: "500"
31772
+ }
31773
+ },
31774
+ children: image.filename
31775
+ }
31776
+ )
31777
+ ]
31778
+ }
31779
+ ),
31780
+ /* @__PURE__ */ jsx(
31781
+ ImageVariantsModal,
31782
+ {
31783
+ image,
31784
+ isOpen: showVariants,
31785
+ onClose: () => setShowVariants(false),
31786
+ onSelect,
31787
+ onDelete,
31788
+ onCrop,
31789
+ onVariantDeleted: handleVariantDeleted,
31790
+ allowedActions: {
31791
+ select: allowedActions.select,
31792
+ download: allowedActions.download,
31793
+ copy: allowedActions.copy,
31794
+ delete: allowedActions.delete,
31795
+ crop: allowedActions.crop
31796
+ }
31797
+ }
31798
+ )
31799
+ ] });
30751
31800
  }
30752
31801
  function Loader({ text = "Cargando..." }) {
30753
31802
  return /* @__PURE__ */ jsxs(
@@ -30851,7 +31900,9 @@ function Gallery({
30851
31900
  download: true,
30852
31901
  copy: true,
30853
31902
  delete: true,
30854
- crop: true
31903
+ crop: true,
31904
+ variants: true
31905
+ // Nueva acción para ver variantes
30855
31906
  }
30856
31907
  }) {
30857
31908
  const [filters, setFilters] = useState({
@@ -31110,240 +32161,21 @@ function TabUpload({ file, setFile, previewUrl, setPreviewUrl, fileInputRef, onU
31110
32161
  }
31111
32162
  );
31112
32163
  }
31113
- class AuthService {
31114
- constructor() {
31115
- this.currentToken = null;
31116
- this.tokenExpiry = null;
31117
- this.refreshPromise = null;
31118
- }
31119
- /**
31120
- * Authenticate and get JWT token
31121
- * Maps to: POST /auth/token
31122
- */
31123
- async authenticate(apiKey, publicKey, prod = false) {
31124
- try {
31125
- const response = await callApi({
31126
- apiKey: "temp",
31127
- // We don't use this for auth endpoint
31128
- endpoint: "/token",
31129
- method: "POST",
31130
- body: {
31131
- api_key: apiKey,
31132
- public_key: publicKey
31133
- },
31134
- prod,
31135
- basePath: "/auth",
31136
- customHeaders: {
31137
- // Override Authorization for auth endpoint
31138
- Authorization: void 0
31139
- }
31140
- });
31141
- if (response && response.token) {
31142
- this.currentToken = response.token;
31143
- this.tokenExpiry = Date.now() + response.expires_in * 1e3 - 3e4;
31144
- return {
31145
- success: true,
31146
- token: response.token,
31147
- expires_in: response.expires_in,
31148
- portal: response.portal,
31149
- policies: response.policies
31150
- };
31151
- }
31152
- throw new Error("Invalid authentication response");
31153
- } catch (error) {
31154
- console.error("Authentication failed:", error);
31155
- return {
31156
- success: false,
31157
- error: error.message
31158
- };
31159
- }
31160
- }
31161
- /**
31162
- * Get current valid token (auto-refresh if needed)
31163
- */
31164
- async getValidToken(apiKey, publicKey, prod = false) {
31165
- if (!this.currentToken || Date.now() > this.tokenExpiry) {
31166
- if (!this.refreshPromise) {
31167
- this.refreshPromise = this.authenticate(apiKey, publicKey, prod);
31168
- }
31169
- const result = await this.refreshPromise;
31170
- this.refreshPromise = null;
31171
- if (!result.success) {
31172
- throw new Error(`Authentication failed: ${result.error}`);
31173
- }
31174
- }
31175
- return this.currentToken;
31176
- }
31177
- /**
31178
- * Check if current token is valid
31179
- */
31180
- isTokenValid() {
31181
- return this.currentToken && Date.now() < this.tokenExpiry;
31182
- }
31183
- /**
31184
- * Clear current token (logout)
31185
- */
31186
- clearToken() {
31187
- this.currentToken = null;
31188
- this.tokenExpiry = null;
31189
- this.refreshPromise = null;
31190
- }
31191
- /**
31192
- * Get token info
31193
- */
31194
- getTokenInfo() {
31195
- if (!this.currentToken) return null;
31196
- return {
31197
- hasToken: !!this.currentToken,
31198
- isValid: this.isTokenValid(),
31199
- expiresAt: new Date(this.tokenExpiry),
31200
- expiresIn: Math.max(0, this.tokenExpiry - Date.now())
31201
- };
31202
- }
31203
- }
31204
- const API_URLS = {
31205
- DEV: "https://led-dev-limbo-dev.eu.els.local",
31206
- // PREPRODUCCIÓN - Updated URL
31207
- // DEV: "http://localhost", // LOCAL - Para desarrollo local
31208
- PROD: "https://limbo.lefebvre.com"
31209
- };
31210
- let globalConfig = {
31211
- // Legacy configuration
31212
- apiKey: null,
31213
- publicKey: null,
31214
- prod: false,
31215
- // New JWT v2 configuration
31216
- auth: {
31217
- apiKey: null,
31218
- publicKey: null,
31219
- authMode: "jwt",
31220
- // "jwt" | "legacy"
31221
- portal: null,
31222
- tokenStorage: "memory"
31223
- }
31224
- };
31225
- let authServiceInstance = null;
31226
- function configureApiClient(config) {
31227
- if (config.auth) {
31228
- globalConfig.auth = { ...globalConfig.auth, ...config.auth };
31229
- globalConfig.prod = config.prod || globalConfig.prod;
31230
- if (globalConfig.auth.authMode === "jwt" && globalConfig.auth.apiKey) {
31231
- if (!authServiceInstance) {
31232
- authServiceInstance = new AuthService();
31233
- }
31234
- }
31235
- } else {
31236
- globalConfig = {
31237
- ...globalConfig,
31238
- ...config,
31239
- auth: {
31240
- ...globalConfig.auth,
31241
- authMode: config.apiKey ? "legacy" : "jwt"
31242
- }
31243
- };
31244
- }
31245
- }
31246
- function getBaseUrl({ prod = false } = {}) {
31247
- return prod ? API_URLS.PROD : API_URLS.DEV;
31248
- }
31249
- async function getHeaders({ isFormData = false, useJWT = true, customHeaders = {} } = {}) {
31250
- const headers = {
31251
- "Content-Type": "application/json"
31252
- };
31253
- if (useJWT) {
31254
- const authConfig = globalConfig.auth;
31255
- if (authConfig.authMode === "jwt" && authConfig.apiKey) {
31256
- try {
31257
- if (!authServiceInstance) {
31258
- authServiceInstance = new AuthService();
31259
- }
31260
- const token = await authServiceInstance.getValidToken(
31261
- authConfig.apiKey,
31262
- authConfig.publicKey || globalConfig.publicKey,
31263
- globalConfig.prod
31264
- );
31265
- headers.Authorization = `Bearer ${token}`;
31266
- } catch (error) {
31267
- console.error("JWT authentication failed:", error);
31268
- throw new Error("Authentication required");
31269
- }
31270
- } else if (authConfig.authMode === "legacy" && globalConfig.apiKey) {
31271
- headers["X-API-Key"] = globalConfig.apiKey;
31272
- }
31273
- }
31274
- if (isFormData) {
31275
- delete headers["Content-Type"];
31276
- }
31277
- return { ...headers, ...customHeaders };
31278
- }
31279
- function configureJWTFromConfigManager(configManager) {
31280
- const authConfig = configManager.getAuthConfig();
31281
- configureApiClient({
31282
- auth: authConfig,
31283
- prod: configManager.get("prod") || false
31284
- });
31285
- }
31286
- async function callApi({
31287
- endpoint,
31288
- method = "GET",
31289
- body = null,
31290
- prod = false,
31291
- basePath = "",
31292
- customHeaders = {},
31293
- isFormData = false,
31294
- useJWT = true
31295
- // New parameter to control JWT usage
31296
- }) {
31297
- try {
31298
- const useProd = prod || globalConfig.prod;
31299
- const headers = await getHeaders({
31300
- isFormData,
31301
- useJWT,
31302
- customHeaders
31303
- });
31304
- const res = await fetch(`${getBaseUrl({ prod: useProd })}${basePath}${endpoint}`, {
31305
- method,
31306
- headers,
31307
- body: body ? isFormData ? body : JSON.stringify(body) : void 0
31308
- });
31309
- if (!res.ok) {
31310
- let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
31311
- try {
31312
- const result = await res.json();
31313
- if (result.error) {
31314
- errorMessage = `${result.error.code}: ${result.error.message}`;
31315
- } else if (result.message) {
31316
- errorMessage = result.message;
31317
- }
31318
- } catch {
31319
- }
31320
- throw new Error(errorMessage);
31321
- }
31322
- return res.json();
31323
- } catch (err) {
31324
- throw new Error(`API Error: ${err.message}`);
31325
- }
32164
+ const BASE_PATH$3 = "/api";
32165
+ function getAiServices(prod = false) {
32166
+ return callApi({ endpoint: "/ai/services", prod, basePath: BASE_PATH$3 });
31326
32167
  }
31327
- const BASE_PATH$4 = "/api/atenea";
31328
- function getAiServices(apiKey, prod = false) {
31329
- return callApi({ endpoint: "/ai/services", prod, basePath: BASE_PATH$4 });
31330
- }
31331
- function generateAiImage(apiKey, params, prod = false) {
31332
- return callApi({ endpoint: "/ai/generate", method: "POST", body: params, prod, basePath: BASE_PATH$4 });
32168
+ function generateAiImage(params, prod = false) {
32169
+ return callApi({ endpoint: "/ai/generate", method: "POST", body: params, prod, basePath: BASE_PATH$3 });
31333
32170
  }
31334
32171
  const CACHE_TTL$4 = 24 * 60 * 60 * 1e3;
31335
32172
  const cache$4 = /* @__PURE__ */ new Map();
31336
- function useAiServices(apiKey, prod = false) {
32173
+ function useAiServices(prod = false) {
31337
32174
  const [services, setServices] = useState([]);
31338
32175
  const [loading, setLoading] = useState(true);
31339
32176
  const [error, setError] = useState(null);
31340
32177
  useEffect(() => {
31341
- if (!apiKey) {
31342
- setError("API Key no proporcionada");
31343
- setLoading(false);
31344
- return;
31345
- }
31346
- const cacheKey = `${apiKey}-${prod}`;
32178
+ const cacheKey = `ai-services-${prod}`;
31347
32179
  const cached = cache$4.get(cacheKey);
31348
32180
  const now = Date.now();
31349
32181
  if (cached && now - cached.timestamp < CACHE_TTL$4) {
@@ -31354,9 +32186,9 @@ function useAiServices(apiKey, prod = false) {
31354
32186
  let isMounted = true;
31355
32187
  const fetchServices = async () => {
31356
32188
  try {
31357
- const data = await getAiServices(apiKey, prod);
32189
+ const data = await getAiServices(prod);
31358
32190
  if (!isMounted) return;
31359
- const result = data.result || [];
32191
+ const result = data?.data?.images || [];
31360
32192
  setServices(result);
31361
32193
  cache$4.set(cacheKey, { data: result, timestamp: now });
31362
32194
  } catch (err) {
@@ -31369,14 +32201,14 @@ function useAiServices(apiKey, prod = false) {
31369
32201
  return () => {
31370
32202
  isMounted = false;
31371
32203
  };
31372
- }, [apiKey, prod]);
32204
+ }, [prod]);
31373
32205
  const invalidateCache = () => {
31374
- cache$4.delete(`${apiKey}-${prod}`);
32206
+ cache$4.delete(`ai-services-${prod}`);
31375
32207
  };
31376
32208
  return { services, loading, error, invalidateCache };
31377
32209
  }
31378
- const BASE_PATH$3 = "/api/atenea";
31379
- function getImageParams(apiKey, { services = null, form = false } = {}, prod = false) {
32210
+ const BASE_PATH$2 = "/api/atenea";
32211
+ function getImageParams({ services = null, form = false } = {}, prod = false) {
31380
32212
  const params = {};
31381
32213
  if (services) params.services = services;
31382
32214
  if (form !== false) params.form = form;
@@ -31385,19 +32217,19 @@ function getImageParams(apiKey, { services = null, form = false } = {}, prod = f
31385
32217
  endpoint: `/ai/params${queryString}`,
31386
32218
  method: "GET",
31387
32219
  prod,
31388
- basePath: BASE_PATH$3
32220
+ basePath: BASE_PATH$2
31389
32221
  });
31390
32222
  }
31391
32223
  const CACHE_TTL$3 = 24 * 60 * 60 * 1e3;
31392
32224
  const cache$3 = /* @__PURE__ */ new Map();
31393
- function useImageParams(apiKey, prod = false) {
32225
+ function useImageParams(prod = false) {
31394
32226
  const [params, setParams] = useState(null);
31395
32227
  const [loading, setLoading] = useState(false);
31396
32228
  const [error, setError] = useState(null);
31397
32229
  const fetchParams = async (service) => {
31398
32230
  setLoading(true);
31399
32231
  setError(null);
31400
- const cacheKey = `${apiKey}-${prod}-${service}`;
32232
+ const cacheKey = `${prod}-${service}`;
31401
32233
  const now = Date.now();
31402
32234
  const cached = cache$3.get(cacheKey);
31403
32235
  if (cached && now - cached.timestamp < CACHE_TTL$3) {
@@ -31407,13 +32239,13 @@ function useImageParams(apiKey, prod = false) {
31407
32239
  }
31408
32240
  try {
31409
32241
  const data = await getImageParams(
31410
- apiKey,
31411
32242
  { services: service, form: false },
31412
32243
  prod
31413
32244
  );
31414
- setParams(data.result || data);
31415
- cache$3.set(cacheKey, { data: data.result || data, timestamp: now });
31416
- return data.result || data;
32245
+ const result = data?.result?.data || data?.data || data?.result || data;
32246
+ setParams(result);
32247
+ cache$3.set(cacheKey, { data: result, timestamp: now });
32248
+ return result;
31417
32249
  } catch (err) {
31418
32250
  setError(err.message);
31419
32251
  setParams(null);
@@ -31428,17 +32260,34 @@ function useImageParams(apiKey, prod = false) {
31428
32260
  };
31429
32261
  return { params, loading, error, fetchParams, reset };
31430
32262
  }
31431
- function TabAI({ apiKey, prod, disabled, onGenerated }) {
31432
- const aiServicesHook = useAiServices(apiKey, prod);
31433
- const imageParamsHook = useImageParams(apiKey, prod);
32263
+ function TabAI({ prod, disabled, onSelected, onGenerated }) {
32264
+ const aiServicesHook = useAiServices(prod);
32265
+ const imageParamsHook = useImageParams(prod);
31434
32266
  const [selectedService, setSelectedService] = useState("");
31435
32267
  const [dynamicForm, setDynamicForm] = useState({});
31436
32268
  const [aiLoading, setAiLoading] = useState(false);
31437
32269
  const [aiError, setAiError] = useState(null);
31438
32270
  const [aiImage, setAiImage] = useState(null);
32271
+ const serviceLabels = {
32272
+ // IA Services
32273
+ "dall-e-2": "Dalle2",
32274
+ "dall-e-3": "Dalle3",
32275
+ "freepik-classic": "Freepik Classic",
32276
+ "freepik-mystic": "Freepik Mystic",
32277
+ "freepik-google": "Freepik Google",
32278
+ "freepik-flux": "Freepik Flux",
32279
+ // Stock Services
32280
+ "shutterstock": "Shutterstock",
32281
+ "freepikstock": "Freepik"
32282
+ };
32283
+ const getServiceLabel = (service) => {
32284
+ return serviceLabels[service] || service;
32285
+ };
31439
32286
  React.useEffect(() => {
31440
32287
  if (!aiServicesHook.loading && aiServicesHook.services.length === 1) {
31441
- setSelectedService(aiServicesHook.services[0]);
32288
+ const service = aiServicesHook.services[0];
32289
+ setSelectedService(service);
32290
+ imageParamsHook.fetchParams(service, true);
31442
32291
  }
31443
32292
  }, [aiServicesHook.loading, aiServicesHook.services]);
31444
32293
  const handleServiceChange = async (e) => {
@@ -31471,22 +32320,49 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31471
32320
  setAiError(null);
31472
32321
  setAiImage(null);
31473
32322
  try {
31474
- const data = await generateAiImage(apiKey, dynamicForm, prod);
31475
- const result = data.result || data;
31476
- if (result && onGenerated) {
31477
- const base64Data = result[0].replace(/^data:image\/\w+;base64,/, "");
31478
- const byteCharacters = atob(base64Data);
31479
- const byteNumbers = new Array(byteCharacters.length);
31480
- for (let i = 0; i < byteCharacters.length; i++) {
31481
- byteNumbers[i] = byteCharacters.charCodeAt(i);
31482
- }
31483
- const byteArray = new Uint8Array(byteNumbers);
31484
- const file = new File([byteArray], "ai-image.webp", {
31485
- type: "image/webp"
31486
- });
31487
- onGenerated(file);
32323
+ const response = await generateAiImage(dynamicForm, prod);
32324
+ let imageData = null;
32325
+ if (response?.data?.images && Array.isArray(response.data.images) && response.data.images.length > 0) {
32326
+ imageData = response.data.images[0];
32327
+ } else if (response?.result?.images && Array.isArray(response.result.images) && response.result.images.length > 0) {
32328
+ imageData = response.result.images[0];
32329
+ } else if (Array.isArray(response) && response.length > 0) {
32330
+ imageData = response[0];
32331
+ } else if (typeof response === "string") {
32332
+ imageData = response;
32333
+ }
32334
+ if (imageData && onGenerated) {
32335
+ if (imageData.startsWith("http")) {
32336
+ try {
32337
+ const imageResponse = await fetch(imageData);
32338
+ if (!imageResponse.ok) {
32339
+ throw new Error(`Failed to download image: ${imageResponse.status}`);
32340
+ }
32341
+ const blob = await imageResponse.blob();
32342
+ const file = new File([blob], "ai-image.png", {
32343
+ type: blob.type || "image/png"
32344
+ });
32345
+ onGenerated(file);
32346
+ } catch (downloadError) {
32347
+ throw new Error(`Error downloading image: ${downloadError.message}`);
32348
+ }
32349
+ } else {
32350
+ const cleanBase64 = imageData.replace(/^data:image\/\w+;base64,/, "");
32351
+ const byteCharacters = atob(cleanBase64);
32352
+ const byteNumbers = new Array(byteCharacters.length);
32353
+ for (let i = 0; i < byteCharacters.length; i++) {
32354
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
32355
+ }
32356
+ const byteArray = new Uint8Array(byteNumbers);
32357
+ const file = new File([byteArray], "ai-image.webp", {
32358
+ type: "image/webp"
32359
+ });
32360
+ onGenerated(file);
32361
+ }
32362
+ } else {
32363
+ throw new Error("No se pudo extraer la imagen de la respuesta");
31488
32364
  }
31489
- setAiImage(result || null);
32365
+ setAiImage(response?.data?.images || response?.result?.images || null);
31490
32366
  } catch (err) {
31491
32367
  setAiError(err.message || "Error al generar la imagen");
31492
32368
  } finally {
@@ -31496,25 +32372,39 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31496
32372
  const renderDynamicForm = () => {
31497
32373
  const params = imageParamsHook.params?.[selectedService]?.parameters;
31498
32374
  if (!params) return null;
32375
+ const visibleFields = Object.entries(params).filter(([_, config]) => !config.disabled);
32376
+ const useGridLayout = visibleFields.length > 3;
31499
32377
  return /* @__PURE__ */ jsxs(
31500
32378
  "form",
31501
32379
  {
31502
32380
  onSubmit: handleDynamicFormSubmit,
31503
32381
  "data-type": "ai",
31504
- className: "flex flex-col gap-4 mt-4 border-t-1 pt-5 border-brand-blue-200",
32382
+ className: "flex flex-col gap-3 mt-4 border-t-1 pt-4 border-brand-blue-200",
31505
32383
  "aria-label": "Formulario generación IA",
31506
32384
  children: [
31507
- Object.entries(params).map(([key, config]) => {
31508
- if (config.disabled) {
31509
- return null;
32385
+ /* @__PURE__ */ jsx("div", { className: useGridLayout ? "grid grid-cols-1 md:grid-cols-2 gap-3" : "flex flex-col gap-3", children: visibleFields.map(([key, config]) => {
32386
+ let placeholder = config.placeholder || "";
32387
+ if (!placeholder) {
32388
+ if (config.type === "integer") {
32389
+ placeholder = config.minValue && config.maxValue ? `Entre ${config.minValue} y ${config.maxValue}` : config.minValue ? `Mínimo ${config.minValue}` : config.maxValue ? `Máximo ${config.maxValue}` : "Introduce un número";
32390
+ } else if (key.toLowerCase().includes("prompt")) {
32391
+ placeholder = "Describe la imagen que deseas generar...";
32392
+ } else if (key.toLowerCase().includes("negative")) {
32393
+ placeholder = "Elementos a evitar en la imagen...";
32394
+ } else {
32395
+ placeholder = `Introduce ${(config.label || key).toLowerCase()}`;
32396
+ }
31510
32397
  }
31511
32398
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
31512
- /* @__PURE__ */ jsx(
32399
+ /* @__PURE__ */ jsxs(
31513
32400
  "label",
31514
32401
  {
31515
32402
  htmlFor: `ai-${key}`,
31516
- className: "text-sm font-medium text-brand-blue-1000",
31517
- children: config.label || key
32403
+ className: "text-xs font-medium text-brand-blue-900",
32404
+ children: [
32405
+ config.label || key,
32406
+ config.required && /* @__PURE__ */ jsx("span", { className: "text-red-600 ml-1", children: "*" })
32407
+ ]
31518
32408
  }
31519
32409
  ),
31520
32410
  config.options ? /* @__PURE__ */ jsx(
@@ -31526,6 +32416,7 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31526
32416
  onChange: handleDynamicFormChange,
31527
32417
  className: "limbo-input",
31528
32418
  disabled,
32419
+ title: config.label || key,
31529
32420
  children: config.options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
31530
32421
  }
31531
32422
  ) : /* @__PURE__ */ jsx(
@@ -31540,11 +32431,13 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31540
32431
  disabled,
31541
32432
  required: config.required,
31542
32433
  min: config.minValue,
31543
- max: config.maxValue
32434
+ max: config.maxValue,
32435
+ placeholder,
32436
+ title: config.label || key
31544
32437
  }
31545
32438
  )
31546
32439
  ] }, key);
31547
- }),
32440
+ }) }),
31548
32441
  /* @__PURE__ */ jsx(
31549
32442
  "button",
31550
32443
  {
@@ -31575,7 +32468,7 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31575
32468
  {
31576
32469
  id: "aiSelectDescription",
31577
32470
  className: "text-lg font-semibold text-brand-blue-1000",
31578
- children: "Selecciona el modelo IA"
32471
+ children: aiServicesHook.services.length === 1 ? `Generación con ${getServiceLabel(selectedService)}` : "Selecciona el modelo IA"
31579
32472
  }
31580
32473
  ),
31581
32474
  aiServicesHook.services.length > 1 && /* @__PURE__ */ jsxs(
@@ -31590,7 +32483,7 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31590
32483
  className: "limbo-input",
31591
32484
  children: [
31592
32485
  /* @__PURE__ */ jsx("option", { value: "", children: "-- Selecciona --" }),
31593
- aiServicesHook.services.map((service) => /* @__PURE__ */ jsx("option", { value: service, children: service }, service))
32486
+ aiServicesHook.services.map((service) => /* @__PURE__ */ jsx("option", { value: service, children: getServiceLabel(service) }, service))
31594
32487
  ]
31595
32488
  }
31596
32489
  ),
@@ -31612,26 +32505,24 @@ function TabAI({ apiKey, prod, disabled, onGenerated }) {
31612
32505
  ] }, "img-" + index))
31613
32506
  ] });
31614
32507
  }
31615
- const BASE_PATH$2 = "/api/atenea";
31616
- function getStockServices(apiKey, prod = false) {
31617
- return callApi({ endpoint: "/stock/services", prod, basePath: BASE_PATH$2 });
32508
+ const BASE_PATH$1 = "/api";
32509
+ function getStockServices(prod = false) {
32510
+ return callApi({ endpoint: "/stock/services", prod, basePath: BASE_PATH$1 });
31618
32511
  }
31619
- function searchStockImages(apiKey, params, prod = false) {
31620
- return callApi({ endpoint: "/stock/search", method: "POST", body: params, prod, basePath: BASE_PATH$2 });
32512
+ function searchStockImages(params, prod = false) {
32513
+ return callApi({ endpoint: "/stock/search", method: "POST", body: params, prod, basePath: BASE_PATH$1 });
32514
+ }
32515
+ function downloadStockImage(params, prod = false) {
32516
+ return callApi({ endpoint: "/stock/download", method: "POST", body: params, prod, basePath: BASE_PATH$1 });
31621
32517
  }
31622
32518
  const CACHE_TTL$2 = 24 * 60 * 60 * 1e3;
31623
32519
  const cache$2 = /* @__PURE__ */ new Map();
31624
- function useStockServices(apiKey, prod = false) {
32520
+ function useStockServices(prod = false) {
31625
32521
  const [services, setServices] = useState([]);
31626
32522
  const [loading, setLoading] = useState(true);
31627
32523
  const [error, setError] = useState(null);
31628
32524
  useEffect(() => {
31629
- if (!apiKey) {
31630
- setError("API Key no proporcionada");
31631
- setLoading(false);
31632
- return;
31633
- }
31634
- const cacheKey = `${apiKey}-${prod}`;
32525
+ const cacheKey = `stock-services-${prod}`;
31635
32526
  const cached = cache$2.get(cacheKey);
31636
32527
  const now = Date.now();
31637
32528
  if (cached && now - cached.timestamp < CACHE_TTL$2) {
@@ -31642,9 +32533,9 @@ function useStockServices(apiKey, prod = false) {
31642
32533
  let isMounted = true;
31643
32534
  const fetchServices = async () => {
31644
32535
  try {
31645
- const data = await getStockServices(apiKey, prod);
32536
+ const data = await getStockServices(prod);
31646
32537
  if (!isMounted) return;
31647
- const result = data.result || [];
32538
+ const result = data?.data?.images || [];
31648
32539
  setServices(result);
31649
32540
  cache$2.set(cacheKey, { data: result, timestamp: now });
31650
32541
  } catch (err) {
@@ -31657,23 +32548,34 @@ function useStockServices(apiKey, prod = false) {
31657
32548
  return () => {
31658
32549
  isMounted = false;
31659
32550
  };
31660
- }, [apiKey, prod]);
32551
+ }, [prod]);
31661
32552
  const invalidateCache = () => {
31662
- cache$2.delete(`${apiKey}-${prod}`);
32553
+ cache$2.delete(`stock-services-${prod}`);
31663
32554
  };
31664
32555
  return { services, loading, error, invalidateCache };
31665
32556
  }
31666
- function TabStock({ apiKey, prod, disabled, onSelected }) {
31667
- const stockServicesHook = useStockServices(apiKey, prod);
31668
- const imageParamsHook = useImageParams(apiKey, prod);
32557
+ function TabStock({ prod, disabled, onSelected }) {
32558
+ const stockServicesHook = useStockServices(prod);
32559
+ const imageParamsHook = useImageParams(prod);
31669
32560
  const [selectedService, setSelectedService] = useState("");
31670
32561
  const [dynamicForm, setDynamicForm] = useState({});
31671
32562
  const [stockLoading, setStockLoading] = useState(false);
31672
32563
  const [stockError, setStockError] = useState(null);
31673
32564
  const [stockImages, setStockImages] = useState([]);
32565
+ const [currentPage, setCurrentPage] = useState(1);
32566
+ const [downloadingId, setDownloadingId] = useState(null);
32567
+ const serviceLabels = {
32568
+ shutterstock: "Shutterstock",
32569
+ freepikstock: "Freepik"
32570
+ };
32571
+ const getServiceLabel = (service) => {
32572
+ return serviceLabels[service] || service;
32573
+ };
31674
32574
  React.useEffect(() => {
31675
32575
  if (!stockServicesHook.loading && stockServicesHook.services.length === 1) {
31676
- setSelectedService(stockServicesHook.services[0]);
32576
+ const service = stockServicesHook.services[0];
32577
+ setSelectedService(service);
32578
+ imageParamsHook.fetchParams(service, true);
31677
32579
  }
31678
32580
  }, [stockServicesHook.loading, stockServicesHook.services]);
31679
32581
  const handleServiceChange = async (e) => {
@@ -31681,6 +32583,7 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31681
32583
  setSelectedService(service);
31682
32584
  setDynamicForm({});
31683
32585
  setStockImages([]);
32586
+ setCurrentPage(1);
31684
32587
  if (service) {
31685
32588
  await imageParamsHook.fetchParams(service, true);
31686
32589
  }
@@ -31700,11 +32603,10 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31700
32603
  const { name, value } = e.target;
31701
32604
  setDynamicForm((prev) => ({ ...prev, [name]: value }));
31702
32605
  };
31703
- const handleDynamicFormSubmit = async (e) => {
31704
- e.preventDefault();
32606
+ const performSearch = async (page = 1) => {
31705
32607
  setStockLoading(true);
31706
32608
  setStockError(null);
31707
- setStockImages([]);
32609
+ if (page === 1) setStockImages([]);
31708
32610
  try {
31709
32611
  const params = imageParamsHook.params?.[selectedService]?.parameters;
31710
32612
  const formData = { ...dynamicForm };
@@ -31712,52 +32614,93 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31712
32614
  if (!(key in formData)) formData[key] = cfg.default ?? "";
31713
32615
  });
31714
32616
  formData.service = selectedService;
31715
- const data = await searchStockImages(apiKey, formData, prod);
31716
- const result = data.result || [];
32617
+ formData.page = page;
32618
+ const data = await searchStockImages(formData, prod);
32619
+ const result = data?.result?.data || [];
31717
32620
  setStockImages(Array.isArray(result) ? result : []);
32621
+ setCurrentPage(page);
31718
32622
  } catch (err) {
31719
32623
  setStockError(err.message || "Error al buscar imágenes");
31720
32624
  } finally {
31721
32625
  setStockLoading(false);
31722
32626
  }
31723
32627
  };
32628
+ const handleDynamicFormSubmit = async (e) => {
32629
+ e.preventDefault();
32630
+ const query = dynamicForm.query || dynamicForm.search || dynamicForm.term || "";
32631
+ if (query.trim().length < 5) {
32632
+ setStockError("La búsqueda debe tener al menos 5 caracteres");
32633
+ return;
32634
+ }
32635
+ await performSearch(1);
32636
+ };
32637
+ const handlePageChange = (newPage) => {
32638
+ if (newPage < 1) return;
32639
+ performSearch(newPage);
32640
+ };
31724
32641
  const handleImageSelect = async (img) => {
31725
- setStockLoading(true);
32642
+ setDownloadingId(img.id);
31726
32643
  setStockError(null);
31727
32644
  try {
31728
- let imageUrl = img.url || img;
31729
- let response = await fetch(imageUrl);
31730
- let blob = await response.blob();
31731
- let file = new File([blob], "stock-image.jpg", { type: blob.type });
31732
- if (onSelected) onSelected(file);
32645
+ const downloadParams = {
32646
+ service: selectedService,
32647
+ image_id: img.id
32648
+ };
32649
+ const downloadData = await downloadStockImage(downloadParams, prod);
32650
+ if (downloadData?.result?.url || downloadData?.result?.download_url) {
32651
+ const imageUrl = downloadData.result.url || downloadData.result.download_url;
32652
+ const response = await fetch(imageUrl);
32653
+ const blob = await response.blob();
32654
+ const filename = downloadData.result.filename || `stock-${img.id}.jpg`;
32655
+ const file = new File([blob], filename, {
32656
+ type: blob.type || "image/jpeg"
32657
+ });
32658
+ if (onSelected) onSelected(file);
32659
+ } else {
32660
+ throw new Error("No se pudo obtener la URL de descarga");
32661
+ }
31733
32662
  } catch (err) {
31734
32663
  setStockError(
31735
32664
  err.message || "No se pudo recuperar la imagen seleccionada"
31736
32665
  );
31737
32666
  } finally {
31738
- setStockLoading(false);
32667
+ setDownloadingId(null);
31739
32668
  }
31740
32669
  };
31741
32670
  const renderDynamicForm = () => {
31742
32671
  const params = imageParamsHook.params?.[selectedService]?.parameters;
31743
32672
  if (!params) return null;
32673
+ const visibleFields = Object.entries(params).filter(([_, config]) => !config.disabled);
32674
+ const useGridLayout = visibleFields.length > 3;
31744
32675
  return /* @__PURE__ */ jsxs(
31745
32676
  "form",
31746
32677
  {
31747
32678
  onSubmit: handleDynamicFormSubmit,
31748
32679
  "data-type": "stock",
31749
- className: "flex flex-col gap-4 mt-4 border-t-1 pt-5 border-brand-blue-200",
32680
+ className: "flex flex-col gap-3 mt-4 border-t-1 pt-4 border-brand-blue-200",
31750
32681
  "aria-label": "Formulario búsqueda de imágenes de Stock",
31751
32682
  children: [
31752
- Object.entries(params).map(([key, config]) => {
31753
- if (config.disabled) return null;
32683
+ /* @__PURE__ */ jsx("div", { className: useGridLayout ? "grid grid-cols-1 md:grid-cols-2 gap-3" : "flex flex-col gap-3", children: visibleFields.map(([key, config]) => {
32684
+ let placeholder = config.placeholder || "";
32685
+ if (!placeholder) {
32686
+ if (config.type === "integer") {
32687
+ placeholder = config.minValue && config.maxValue ? `Entre ${config.minValue} y ${config.maxValue}` : config.minValue ? `Mínimo ${config.minValue}` : config.maxValue ? `Máximo ${config.maxValue}` : "Introduce un número";
32688
+ } else if (key.toLowerCase().includes("query") || key.toLowerCase().includes("search") || key.toLowerCase().includes("term")) {
32689
+ placeholder = "Buscar imágenes...";
32690
+ } else {
32691
+ placeholder = `Introduce ${(config.label || key).toLowerCase()}`;
32692
+ }
32693
+ }
31754
32694
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
31755
- /* @__PURE__ */ jsx(
32695
+ /* @__PURE__ */ jsxs(
31756
32696
  "label",
31757
32697
  {
31758
32698
  htmlFor: `stock-${key}`,
31759
- className: "text-sm font-medium text-brand-blue-1000",
31760
- children: config.label || key
32699
+ className: "text-xs font-medium text-brand-blue-900",
32700
+ children: [
32701
+ config.label || key,
32702
+ config.required && /* @__PURE__ */ jsx("span", { className: "text-red-600 ml-1", children: "*" })
32703
+ ]
31761
32704
  }
31762
32705
  ),
31763
32706
  config.options ? /* @__PURE__ */ jsx(
@@ -31769,6 +32712,7 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31769
32712
  onChange: handleDynamicFormChange,
31770
32713
  className: "limbo-input",
31771
32714
  disabled,
32715
+ title: config.label || key,
31772
32716
  children: config.options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
31773
32717
  }
31774
32718
  ) : /* @__PURE__ */ jsx(
@@ -31783,11 +32727,13 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31783
32727
  disabled,
31784
32728
  required: config.required,
31785
32729
  min: config.minValue,
31786
- max: config.maxValue
32730
+ max: config.maxValue,
32731
+ placeholder,
32732
+ title: config.label || key
31787
32733
  }
31788
32734
  )
31789
32735
  ] }, key);
31790
- }),
32736
+ }) }),
31791
32737
  /* @__PURE__ */ jsx(
31792
32738
  "button",
31793
32739
  {
@@ -31812,7 +32758,7 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31812
32758
  return /* @__PURE__ */ jsx("div", { className: "alert alert-warning", children: "No hay servicios de Stock disponibles." });
31813
32759
  }
31814
32760
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
31815
- /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-brand-blue-1000", children: "Selecciona el servicio de Stock" }),
32761
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-brand-blue-1000", children: stockServicesHook.services.length === 1 ? `Búsqueda en ${getServiceLabel(selectedService)}` : "Selecciona el servicio de Stock" }),
31816
32762
  stockServicesHook.services.length > 1 && /* @__PURE__ */ jsxs(
31817
32763
  "select",
31818
32764
  {
@@ -31822,7 +32768,7 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31822
32768
  className: "limbo-input",
31823
32769
  children: [
31824
32770
  /* @__PURE__ */ jsx("option", { value: "", children: "-- Selecciona --" }),
31825
- stockServicesHook.services.map((service) => /* @__PURE__ */ jsx("option", { value: service, children: service }, service))
32771
+ stockServicesHook.services.map((service) => /* @__PURE__ */ jsx("option", { value: service, children: getServiceLabel(service) }, service))
31826
32772
  ]
31827
32773
  }
31828
32774
  ),
@@ -31831,62 +32777,117 @@ function TabStock({ apiKey, prod, disabled, onSelected }) {
31831
32777
  imageParamsHook.error && /* @__PURE__ */ jsx("div", { className: "alert alert-danger", children: imageParamsHook.error }),
31832
32778
  selectedService && renderDynamicForm(),
31833
32779
  stockError && /* @__PURE__ */ jsx("div", { className: "alert alert-danger", children: stockError }),
31834
- stockImages.length > 0 && /* @__PURE__ */ jsx(
31835
- "div",
31836
- {
31837
- className: "mt-6 grid grid-cols-2 md:grid-cols-4 gap-4",
31838
- "aria-live": "polite",
31839
- children: stockImages.map((img, idx) => /* @__PURE__ */ jsxs(
31840
- "div",
32780
+ stockImages.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
32781
+ /* @__PURE__ */ jsx(
32782
+ "div",
32783
+ {
32784
+ className: "mt-6 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",
32785
+ "aria-live": "polite",
32786
+ children: stockImages.map((img, idx) => /* @__PURE__ */ jsxs(
32787
+ "div",
32788
+ {
32789
+ className: "border border-brand-blue-200 rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow",
32790
+ children: [
32791
+ /* @__PURE__ */ jsxs("div", { className: "relative aspect-video bg-neutral-100", children: [
32792
+ /* @__PURE__ */ jsx(
32793
+ "img",
32794
+ {
32795
+ src: img.preview || img.thumbnail || img.url,
32796
+ alt: img.title || `Resultado ${idx + 1}`,
32797
+ className: "object-cover w-full h-full",
32798
+ loading: "lazy"
32799
+ }
32800
+ ),
32801
+ img.id && /* @__PURE__ */ jsxs("span", { className: "absolute bottom-1 right-1 bg-black/60 text-white text-xs px-2 py-1 rounded", children: [
32802
+ "ID: ",
32803
+ img.id
32804
+ ] })
32805
+ ] }),
32806
+ /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
32807
+ "button",
32808
+ {
32809
+ className: `limbo-btn w-full text-sm ${downloadingId === img.id ? "limbo-btn-disabled cursor-not-allowed" : "limbo-btn-primary"}`,
32810
+ onClick: () => handleImageSelect(img),
32811
+ disabled: stockLoading || disabled || downloadingId === img.id,
32812
+ children: downloadingId === img.id ? /* @__PURE__ */ jsxs(Fragment, { children: [
32813
+ /* @__PURE__ */ jsx("span", { className: "limbo-loader limbo-loader--sm mr-1" }),
32814
+ "Descargando..."
32815
+ ] }) : "Seleccionar"
32816
+ }
32817
+ ) })
32818
+ ]
32819
+ },
32820
+ img.id || idx
32821
+ ))
32822
+ }
32823
+ ),
32824
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center justify-center gap-2", children: [
32825
+ /* @__PURE__ */ jsxs(
32826
+ "button",
31841
32827
  {
31842
- className: "border border-brand-blue-200 rounded-lg overflow-hidden bg-white shadow-sm flex flex-col items-center",
32828
+ onClick: () => handlePageChange(currentPage - 1),
32829
+ disabled: currentPage === 1 || stockLoading,
32830
+ className: "limbo-btn limbo-btn-secondary px-4 py-2 disabled:opacity-50",
31843
32831
  children: [
31844
- /* @__PURE__ */ jsx(
31845
- "img",
31846
- {
31847
- src: img.url || img,
31848
- alt: `Resultado ${idx + 1}`,
31849
- className: "object-cover w-full h-40"
31850
- }
31851
- ),
31852
- /* @__PURE__ */ jsx(
31853
- "button",
31854
- {
31855
- className: "limbo-btn limbo-btn-primary mt-2",
31856
- onClick: () => handleImageSelect(img),
31857
- disabled: stockLoading || disabled,
31858
- children: "Seleccionar"
31859
- }
31860
- )
32832
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-white icon--sm" }),
32833
+ " Anterior"
31861
32834
  ]
31862
- },
31863
- idx
31864
- ))
31865
- }
31866
- )
32835
+ }
32836
+ ),
32837
+ /* @__PURE__ */ jsxs("span", { className: "px-4 py-2 text-sm text-neutral-700", children: [
32838
+ "Página ",
32839
+ currentPage
32840
+ ] }),
32841
+ /* @__PURE__ */ jsxs(
32842
+ "button",
32843
+ {
32844
+ onClick: () => handlePageChange(currentPage + 1),
32845
+ disabled: stockLoading || stockImages.length === 0,
32846
+ className: "limbo-btn limbo-btn-secondary px-4 py-2 disabled:opacity-50",
32847
+ children: [
32848
+ "Siguiente ",
32849
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-white icon--sm" })
32850
+ ]
32851
+ }
32852
+ )
32853
+ ] })
32854
+ ] }),
32855
+ !stockLoading && stockImages.length === 0 && selectedService && dynamicForm.query && /* @__PURE__ */ jsxs("div", { className: "mt-6 text-center text-neutral-600 py-8 bg-neutral-50 rounded-lg", children: [
32856
+ /* @__PURE__ */ jsx("span", { className: "icon icon-search icon--lg mb-2" }),
32857
+ /* @__PURE__ */ jsxs("p", { children: [
32858
+ 'No se encontraron imágenes para "',
32859
+ dynamicForm.query,
32860
+ '"'
32861
+ ] }),
32862
+ /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: "Intenta con otros términos de búsqueda" })
32863
+ ] })
31867
32864
  ] });
31868
32865
  }
31869
- const BASE_PATH$1 = "/api/atenea";
31870
- function getExternalImageSources(apiKey, prod = false) {
32866
+ const BASE_PATH = "/api";
32867
+ function getExternalImageSources(prod = false) {
32868
+ return callApi({
32869
+ endpoint: "/external/sources",
32870
+ prod,
32871
+ basePath: BASE_PATH
32872
+ });
32873
+ }
32874
+ function getExternalImages(params, prod = false) {
31871
32875
  return callApi({
31872
- endpoint: "/portals/list",
32876
+ endpoint: `/external/search`,
32877
+ method: "POST",
32878
+ body: params,
31873
32879
  prod,
31874
- basePath: BASE_PATH$1
32880
+ basePath: BASE_PATH
31875
32881
  });
31876
32882
  }
31877
32883
  const CACHE_TTL$1 = 24 * 60 * 60 * 1e3;
31878
32884
  const cache$1 = /* @__PURE__ */ new Map();
31879
- function usePortalSources(apiKey, prod = false) {
32885
+ function usePortalSources(prod = false) {
31880
32886
  const [sources, setSources] = useState([]);
31881
32887
  const [loading, setLoading] = useState(true);
31882
32888
  const [error, setError] = useState(null);
31883
32889
  useEffect(() => {
31884
- if (!apiKey) {
31885
- setError("API Key no proporcionada");
31886
- setLoading(false);
31887
- return;
31888
- }
31889
- const cacheKey = `${apiKey}-${prod}`;
32890
+ const cacheKey = `portal-sources-${prod}`;
31890
32891
  const cached = cache$1.get(cacheKey);
31891
32892
  const now = Date.now();
31892
32893
  if (cached && now - cached.timestamp < CACHE_TTL$1) {
@@ -31897,11 +32898,15 @@ function usePortalSources(apiKey, prod = false) {
31897
32898
  let isMounted = true;
31898
32899
  const fetchSources = async () => {
31899
32900
  try {
31900
- const data = await getExternalImageSources(apiKey, prod);
32901
+ const data = await getExternalImageSources(prod);
31901
32902
  if (!isMounted) return;
31902
- const result = data.result || [];
31903
- setSources(result);
31904
- cache$1.set(cacheKey, { data: result, timestamp: now });
32903
+ const result = data?.data?.sources || {};
32904
+ const sourcesArray = Object.entries(result).map(([id, source]) => ({
32905
+ id,
32906
+ ...source
32907
+ }));
32908
+ setSources(sourcesArray);
32909
+ cache$1.set(cacheKey, { data: sourcesArray, timestamp: now });
31905
32910
  } catch (err) {
31906
32911
  if (isMounted) setError(err.message);
31907
32912
  } finally {
@@ -31912,31 +32917,475 @@ function usePortalSources(apiKey, prod = false) {
31912
32917
  return () => {
31913
32918
  isMounted = false;
31914
32919
  };
31915
- }, [apiKey, prod]);
32920
+ }, [prod]);
31916
32921
  const invalidateCache = () => {
31917
- cache$1.delete(`${apiKey}-${prod}`);
32922
+ cache$1.delete(`portal-sources-${prod}`);
31918
32923
  };
31919
32924
  return { sources, loading, error, invalidateCache };
31920
32925
  }
31921
- function TabPortals({ apiKey, prod, disabled }) {
31922
- const portalSourcesHook = usePortalSources(apiKey, prod);
31923
- const [portalSelected, setPortalSelected] = useState("");
31924
- const handlePortalSelect = (e) => {
31925
- setPortalSelected(e.target.value);
32926
+ const ValidatedImage = ({ src, alt, className, onError }) => {
32927
+ const [imageStatus, setImageStatus] = React.useState("loading");
32928
+ const handleImageLoad = () => {
32929
+ setImageStatus("loaded");
32930
+ };
32931
+ const handleImageError = () => {
32932
+ setImageStatus("error");
32933
+ if (onError) onError();
32934
+ };
32935
+ if (imageStatus === "error") {
32936
+ return null;
32937
+ }
32938
+ return /* @__PURE__ */ jsx(
32939
+ "img",
32940
+ {
32941
+ src,
32942
+ alt,
32943
+ className,
32944
+ loading: "lazy",
32945
+ onLoad: handleImageLoad,
32946
+ onError: handleImageError
32947
+ }
32948
+ );
32949
+ };
32950
+ function TabPortals({ prod, disabled, onSelected }) {
32951
+ const portalSourcesHook = usePortalSources(prod);
32952
+ const loadSavedState = (key, defaultValue) => {
32953
+ try {
32954
+ const saved = sessionStorage.getItem(`limbo_portals_${key}`);
32955
+ return saved ? JSON.parse(saved) : defaultValue;
32956
+ } catch {
32957
+ return defaultValue;
32958
+ }
32959
+ };
32960
+ const [selectedPortals, setSelectedPortals] = useState(() => loadSavedState("selectedPortals", []));
32961
+ const [searchName, setSearchName] = useState(() => loadSavedState("searchName", ""));
32962
+ const [limit, setLimit] = useState(() => loadSavedState("limit", 20));
32963
+ const [currentPage, setCurrentPage] = useState(() => loadSavedState("currentPage", 1));
32964
+ const [loading, setLoading] = useState(false);
32965
+ const [error, setError] = useState(null);
32966
+ const [images, setImages] = useState(() => loadSavedState("images", []));
32967
+ const [portalResults, setPortalResults] = useState(() => loadSavedState("portalResults", {}));
32968
+ const [paginationInfo, setPaginationInfo] = useState(() => loadSavedState("paginationInfo", null));
32969
+ const [downloadingUrl, setDownloadingUrl] = useState(null);
32970
+ const searchCacheRef = useRef({});
32971
+ const [failedImages, setFailedImages] = useState(/* @__PURE__ */ new Set());
32972
+ React.useEffect(() => {
32973
+ sessionStorage.setItem("limbo_portals_selectedPortals", JSON.stringify(selectedPortals));
32974
+ }, [selectedPortals]);
32975
+ React.useEffect(() => {
32976
+ sessionStorage.setItem("limbo_portals_searchName", JSON.stringify(searchName));
32977
+ }, [searchName]);
32978
+ React.useEffect(() => {
32979
+ sessionStorage.setItem("limbo_portals_limit", JSON.stringify(limit));
32980
+ }, [limit]);
32981
+ React.useEffect(() => {
32982
+ sessionStorage.setItem("limbo_portals_currentPage", JSON.stringify(currentPage));
32983
+ }, [currentPage]);
32984
+ React.useEffect(() => {
32985
+ sessionStorage.setItem("limbo_portals_images", JSON.stringify(images));
32986
+ }, [images]);
32987
+ React.useEffect(() => {
32988
+ sessionStorage.setItem("limbo_portals_portalResults", JSON.stringify(portalResults));
32989
+ }, [portalResults]);
32990
+ React.useEffect(() => {
32991
+ sessionStorage.setItem("limbo_portals_paginationInfo", JSON.stringify(paginationInfo));
32992
+ }, [paginationInfo]);
32993
+ const getCacheKey = (portals, name, limitVal, page) => {
32994
+ return `${portals.sort().join(",")}_${name}_${limitVal}_${page}`;
32995
+ };
32996
+ const handlePortalToggle = (portalKey) => {
32997
+ setSelectedPortals(
32998
+ (prev) => prev.includes(portalKey) ? prev.filter((p) => p !== portalKey) : [...prev, portalKey]
32999
+ );
33000
+ };
33001
+ const handleSelectAll = () => {
33002
+ if (selectedPortals.length === portalSourcesHook.sources.length) {
33003
+ setSelectedPortals([]);
33004
+ } else {
33005
+ setSelectedPortals(portalSourcesHook.sources.map((s) => s.id));
33006
+ }
33007
+ };
33008
+ const performSearch = async (page = 1) => {
33009
+ if (selectedPortals.length === 0) {
33010
+ setError("Selecciona al menos un portal");
33011
+ return;
33012
+ }
33013
+ const cacheKey = getCacheKey(
33014
+ selectedPortals,
33015
+ searchName.trim(),
33016
+ limit,
33017
+ page
33018
+ );
33019
+ const cachedData = searchCacheRef.current[cacheKey];
33020
+ if (cachedData) {
33021
+ setImages(cachedData.images);
33022
+ setPortalResults(cachedData.portalResults);
33023
+ setPaginationInfo(cachedData.paginationInfo);
33024
+ setCurrentPage(page);
33025
+ setError(null);
33026
+ return;
33027
+ }
33028
+ setLoading(true);
33029
+ setError(null);
33030
+ if (page === 1) {
33031
+ setImages([]);
33032
+ setPortalResults({});
33033
+ setPaginationInfo(null);
33034
+ }
33035
+ try {
33036
+ const params = {
33037
+ sources: selectedPortals,
33038
+ // Ya son strings de IDs
33039
+ limit,
33040
+ page,
33041
+ name: searchName.trim()
33042
+ };
33043
+ const data = await getExternalImages(params, prod);
33044
+ const sources = data?.data?.sources || {};
33045
+ const allImages = [];
33046
+ const newPortalResults = {};
33047
+ const seenUrls = /* @__PURE__ */ new Set();
33048
+ let paginationInfo2 = null;
33049
+ Object.entries(sources).forEach(([portalId, portalData]) => {
33050
+ newPortalResults[portalId] = {
33051
+ title: portalData.meta?.title || portalId,
33052
+ status: 200,
33053
+ // Si está en sources, es exitoso
33054
+ response: "OK",
33055
+ count: portalData.images?.length || 0
33056
+ };
33057
+ if (Array.isArray(portalData.images)) {
33058
+ portalData.images.forEach((img) => {
33059
+ const imageUrl = img.url || img.thumbnail;
33060
+ if (!imageUrl || seenUrls.has(imageUrl)) {
33061
+ return;
33062
+ }
33063
+ seenUrls.add(imageUrl);
33064
+ allImages.push({
33065
+ ...img,
33066
+ source: portalId,
33067
+ sourceTitle: portalData.meta?.title || portalId,
33068
+ preview: img.thumbnail || img.url,
33069
+ full: img.url
33070
+ });
33071
+ });
33072
+ if (!paginationInfo2 && portalData.pagination) {
33073
+ paginationInfo2 = portalData.pagination;
33074
+ }
33075
+ }
33076
+ });
33077
+ searchCacheRef.current[cacheKey] = {
33078
+ images: allImages,
33079
+ portalResults: newPortalResults,
33080
+ paginationInfo: paginationInfo2
33081
+ };
33082
+ setImages(allImages);
33083
+ setPortalResults(newPortalResults);
33084
+ setPaginationInfo(paginationInfo2);
33085
+ setCurrentPage(page);
33086
+ setFailedImages(/* @__PURE__ */ new Set());
33087
+ if (allImages.length === 0 && !data?.result?.success) {
33088
+ setError(data?.result?.message || "No se encontraron imágenes");
33089
+ }
33090
+ } catch (err) {
33091
+ setError(err.message || "Error al buscar imágenes en portales");
33092
+ } finally {
33093
+ setLoading(false);
33094
+ }
31926
33095
  };
31927
33096
  const handleSearch = (e) => {
31928
33097
  e.preventDefault();
31929
- if (!portalSelected) return;
31930
- alert("Aquí se buscaría en el portal: " + portalSelected);
33098
+ performSearch(1);
33099
+ };
33100
+ const handlePageChange = (newPage) => {
33101
+ if (newPage < 1) return;
33102
+ performSearch(newPage);
33103
+ };
33104
+ const handleImageSelect = async (img) => {
33105
+ setDownloadingUrl(img.url || img.full);
33106
+ setError(null);
33107
+ try {
33108
+ const imageUrl = img.full || img.url || img.preview;
33109
+ if (!imageUrl) {
33110
+ throw new Error("No se encontró URL de la imagen");
33111
+ }
33112
+ const apiBaseUrl = getBaseUrl({ prod });
33113
+ const proxyUrl = `${apiBaseUrl}/api/atenea/proxy?url=${encodeURIComponent(imageUrl)}`;
33114
+ const response = await fetch(proxyUrl);
33115
+ if (!response.ok) {
33116
+ throw new Error(`Error al descargar: ${response.status}`);
33117
+ }
33118
+ const blob = await response.blob();
33119
+ const filename = img.filename || img.title || `portal-${img.id || Date.now()}.jpg`;
33120
+ const file = new File([blob], filename, {
33121
+ type: blob.type || "image/jpeg"
33122
+ });
33123
+ if (onSelected) onSelected(file);
33124
+ } catch (err) {
33125
+ setError(err.message || "No se pudo recuperar la imagen del portal");
33126
+ } finally {
33127
+ setDownloadingUrl(null);
33128
+ }
31931
33129
  };
31932
- return /* @__PURE__ */ jsxs("div", { children: [
31933
- /* @__PURE__ */ jsx("h3", { className: "mb-2", children: "Selecciona un portal externo" }),
31934
- /* @__PURE__ */ jsxs("select", { value: portalSelected, onChange: handlePortalSelect, disabled: portalSourcesHook.loading || disabled, children: [
31935
- /* @__PURE__ */ jsx("option", { value: "", children: "-- Selecciona --" }),
31936
- Object.entries(portalSourcesHook.sources).map(([key, portal]) => /* @__PURE__ */ jsx("option", { value: key, children: portal.title }, key))
33130
+ if (portalSourcesHook.loading) {
33131
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center py-8", children: [
33132
+ /* @__PURE__ */ jsx("span", { className: "limbo-loader mr-2" }),
33133
+ " Cargando portales disponibles..."
33134
+ ] });
33135
+ }
33136
+ if (!portalSourcesHook.sources.length) {
33137
+ return /* @__PURE__ */ jsx("div", { className: "alert alert-warning", children: "No hay portales externos disponibles." });
33138
+ }
33139
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
33140
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-brand-blue-1000", children: "Buscar en Portales Externos" }),
33141
+ /* @__PURE__ */ jsxs(
33142
+ "form",
33143
+ {
33144
+ onSubmit: handleSearch,
33145
+ className: "flex flex-col gap-4 border-t-1 pt-4 border-brand-blue-200",
33146
+ children: [
33147
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
33148
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
33149
+ /* @__PURE__ */ jsxs("label", { className: "text-sm font-medium text-brand-blue-1000", children: [
33150
+ "Portales (",
33151
+ selectedPortals.length,
33152
+ " seleccionados)"
33153
+ ] }),
33154
+ /* @__PURE__ */ jsx(
33155
+ "button",
33156
+ {
33157
+ type: "button",
33158
+ onClick: handleSelectAll,
33159
+ className: "text-xs text-brand-blue-800 hover:underline",
33160
+ children: selectedPortals.length === portalSourcesHook.sources.length ? "Deseleccionar todos" : "Seleccionar todos"
33161
+ }
33162
+ )
33163
+ ] }),
33164
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 md:grid-cols-3 gap-2", children: portalSourcesHook.sources.map((portal) => /* @__PURE__ */ jsxs(
33165
+ "label",
33166
+ {
33167
+ className: "flex items-center gap-2 p-2 border border-brand-blue-200 rounded cursor-pointer hover:bg-brand-blue-050 transition",
33168
+ children: [
33169
+ /* @__PURE__ */ jsx(
33170
+ "input",
33171
+ {
33172
+ type: "checkbox",
33173
+ checked: selectedPortals.includes(portal.id),
33174
+ onChange: () => handlePortalToggle(portal.id),
33175
+ disabled,
33176
+ className: "w-4 h-4"
33177
+ }
33178
+ ),
33179
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: portal.title })
33180
+ ]
33181
+ },
33182
+ portal.id
33183
+ )) })
33184
+ ] }),
33185
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
33186
+ /* @__PURE__ */ jsx(
33187
+ "label",
33188
+ {
33189
+ htmlFor: "portal-search-name",
33190
+ className: "text-sm font-medium text-brand-blue-1000",
33191
+ children: "Buscar por nombre (opcional)"
33192
+ }
33193
+ ),
33194
+ /* @__PURE__ */ jsx(
33195
+ "input",
33196
+ {
33197
+ id: "portal-search-name",
33198
+ type: "text",
33199
+ value: searchName,
33200
+ onChange: (e) => setSearchName(e.target.value),
33201
+ className: "limbo-input",
33202
+ placeholder: "Deja vacío para ver todas las imágenes",
33203
+ disabled
33204
+ }
33205
+ )
33206
+ ] }),
33207
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
33208
+ /* @__PURE__ */ jsx(
33209
+ "label",
33210
+ {
33211
+ htmlFor: "portal-limit",
33212
+ className: "text-sm font-medium text-brand-blue-1000",
33213
+ children: "Imágenes por página"
33214
+ }
33215
+ ),
33216
+ /* @__PURE__ */ jsxs(
33217
+ "select",
33218
+ {
33219
+ id: "portal-limit",
33220
+ value: limit,
33221
+ onChange: (e) => setLimit(Number(e.target.value)),
33222
+ className: "limbo-input",
33223
+ disabled,
33224
+ children: [
33225
+ /* @__PURE__ */ jsx("option", { value: 10, children: "10" }),
33226
+ /* @__PURE__ */ jsx("option", { value: 20, children: "20" }),
33227
+ /* @__PURE__ */ jsx("option", { value: 50, children: "50" }),
33228
+ /* @__PURE__ */ jsx("option", { value: 100, children: "100" })
33229
+ ]
33230
+ }
33231
+ )
33232
+ ] }),
33233
+ /* @__PURE__ */ jsx(
33234
+ "button",
33235
+ {
33236
+ type: "submit",
33237
+ disabled: loading || disabled || selectedPortals.length === 0,
33238
+ className: `limbo-btn w-full ${loading || selectedPortals.length === 0 ? "cursor-not-allowed limbo-btn-disabled" : "limbo-btn-primary"}`,
33239
+ style: { minHeight: 44 },
33240
+ children: loading ? "Buscando..." : "Buscar imágenes"
33241
+ }
33242
+ )
33243
+ ]
33244
+ }
33245
+ ),
33246
+ portalSourcesHook.error && /* @__PURE__ */ jsx("div", { className: "alert alert-danger", children: portalSourcesHook.error }),
33247
+ error && /* @__PURE__ */ jsx("div", { className: "alert alert-danger", children: error }),
33248
+ Object.keys(portalResults).length > 0 && /* @__PURE__ */ jsxs("div", { className: "bg-neutral-50 rounded-lg p-3", children: [
33249
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold mb-2", children: "Resultados por portal:" }),
33250
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2 text-sm", children: Object.entries(portalResults).map(([portalId, result]) => /* @__PURE__ */ jsxs(
33251
+ "div",
33252
+ {
33253
+ className: `flex items-center justify-between p-2 rounded ${result.status === 200 ? "bg-green-50" : "bg-red-50"}`,
33254
+ children: [
33255
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: result.title }),
33256
+ /* @__PURE__ */ jsx(
33257
+ "span",
33258
+ {
33259
+ className: result.status === 200 ? "text-green-700" : "text-red-700",
33260
+ children: result.status === 200 ? `${result.count} imágenes` : result.response
33261
+ }
33262
+ )
33263
+ ]
33264
+ },
33265
+ portalId
33266
+ )) })
33267
+ ] }),
33268
+ (images.length > 0 || loading) && /* @__PURE__ */ jsxs(Fragment, { children: [
33269
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 relative", "aria-live": "polite", children: [
33270
+ loading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-white/80 z-10 flex items-center justify-center rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
33271
+ /* @__PURE__ */ jsx("span", { className: "limbo-loader" }),
33272
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-brand-blue-800", children: "Buscando imágenes..." })
33273
+ ] }) }),
33274
+ /* @__PURE__ */ jsx(
33275
+ "div",
33276
+ {
33277
+ className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${loading ? "opacity-50" : ""}`,
33278
+ children: images.map((img, idx) => {
33279
+ const imageKey = `${img.source}-${img.id || idx}`;
33280
+ const imageUrl = img.preview || img.thumbnail || img.url || img.full;
33281
+ if (failedImages.has(imageKey)) {
33282
+ return null;
33283
+ }
33284
+ return /* @__PURE__ */ jsxs(
33285
+ "div",
33286
+ {
33287
+ className: "border border-brand-blue-200 rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow",
33288
+ children: [
33289
+ /* @__PURE__ */ jsxs("div", { className: "relative aspect-video bg-neutral-100", children: [
33290
+ /* @__PURE__ */ jsx(
33291
+ ValidatedImage,
33292
+ {
33293
+ src: imageUrl,
33294
+ alt: img.title || img.filename || `Imagen ${idx + 1}`,
33295
+ className: "object-cover w-full h-full",
33296
+ onError: () => {
33297
+ setFailedImages((prev) => {
33298
+ const newSet = new Set(prev);
33299
+ newSet.add(imageKey);
33300
+ return newSet;
33301
+ });
33302
+ }
33303
+ }
33304
+ ),
33305
+ /* @__PURE__ */ jsx("span", { className: "absolute top-1 left-1 bg-black/60 text-white text-xs px-2 py-1 rounded", children: img.sourceTitle }),
33306
+ img.id && /* @__PURE__ */ jsxs("span", { className: "absolute bottom-1 right-1 bg-black/60 text-white text-xs px-2 py-1 rounded", children: [
33307
+ "ID: ",
33308
+ img.id
33309
+ ] })
33310
+ ] }),
33311
+ /* @__PURE__ */ jsxs("div", { className: "p-2", children: [
33312
+ img.title && /* @__PURE__ */ jsx(
33313
+ "p",
33314
+ {
33315
+ className: "text-xs text-neutral-700 mb-1 truncate",
33316
+ title: img.title,
33317
+ children: img.title
33318
+ }
33319
+ ),
33320
+ /* @__PURE__ */ jsx(
33321
+ "button",
33322
+ {
33323
+ className: `limbo-btn w-full text-sm ${downloadingUrl === (img.url || img.full) ? "limbo-btn-disabled cursor-not-allowed" : "limbo-btn-primary"}`,
33324
+ onClick: () => handleImageSelect(img),
33325
+ disabled: loading || disabled || downloadingUrl === (img.url || img.full),
33326
+ children: downloadingUrl === (img.url || img.full) ? /* @__PURE__ */ jsxs(Fragment, { children: [
33327
+ /* @__PURE__ */ jsx("span", { className: "limbo-loader limbo-loader--sm mr-1" }),
33328
+ "Descargando..."
33329
+ ] }) : "Seleccionar"
33330
+ }
33331
+ )
33332
+ ] })
33333
+ ]
33334
+ },
33335
+ imageKey
33336
+ );
33337
+ })
33338
+ }
33339
+ )
33340
+ ] }),
33341
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center justify-center gap-2", children: [
33342
+ /* @__PURE__ */ jsxs(
33343
+ "button",
33344
+ {
33345
+ onClick: () => handlePageChange(currentPage - 1),
33346
+ disabled: currentPage === 1 || loading,
33347
+ className: "limbo-btn limbo-btn-secondary px-4 py-2 disabled:opacity-50" + (currentPage === 1 || loading ? " pointer-events-none cursor-default" : " limbo-btn-primary"),
33348
+ children: [
33349
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-white icon--sm" }),
33350
+ " ",
33351
+ "Anterior"
33352
+ ]
33353
+ }
33354
+ ),
33355
+ /* @__PURE__ */ jsxs("span", { className: "px-4 py-2 text-sm text-neutral-700", children: [
33356
+ "Página ",
33357
+ currentPage,
33358
+ paginationInfo && /* @__PURE__ */ jsxs(Fragment, { children: [
33359
+ " ",
33360
+ "de ",
33361
+ paginationInfo.pages,
33362
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-neutral-500 block", children: [
33363
+ "(",
33364
+ paginationInfo.total,
33365
+ " imágenes totales)"
33366
+ ] })
33367
+ ] })
33368
+ ] }),
33369
+ /* @__PURE__ */ jsxs(
33370
+ "button",
33371
+ {
33372
+ onClick: () => handlePageChange(currentPage + 1),
33373
+ disabled: loading || paginationInfo && currentPage >= paginationInfo.pages,
33374
+ className: "limbo-btn limbo-btn-secondary px-4 py-2 disabled:opacity-50" + (loading || paginationInfo && currentPage >= paginationInfo.pages ? " pointer-events-none cursor-default" : " limbo-btn-primary"),
33375
+ children: [
33376
+ "Siguiente",
33377
+ " ",
33378
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-white icon--sm" })
33379
+ ]
33380
+ }
33381
+ )
33382
+ ] })
31937
33383
  ] }),
31938
- portalSourcesHook.error && /* @__PURE__ */ jsx("div", { className: "text-red-500", children: portalSourcesHook.error }),
31939
- /* @__PURE__ */ jsx("button", { className: "mt-2", onClick: handleSearch, disabled: !portalSelected || disabled, children: "Buscar en portal" })
33384
+ !loading && images.length === 0 && Object.keys(portalResults).length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6 text-center text-neutral-600 py-8 bg-neutral-50 rounded-lg", children: [
33385
+ /* @__PURE__ */ jsx("span", { className: "icon icon-search icon--lg mb-2" }),
33386
+ /* @__PURE__ */ jsx("p", { children: "No se encontraron imágenes en los portales seleccionados" }),
33387
+ searchName && /* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: "Intenta con otros términos o sin filtro de nombre" })
33388
+ ] })
31940
33389
  ] });
31941
33390
  }
31942
33391
  const TABS = [
@@ -32047,8 +33496,32 @@ function UploadForm({
32047
33496
  }
32048
33497
  }
32049
33498
  ),
32050
- activeTab === "stock" && /* @__PURE__ */ jsx(TabStock, { apiKey, prod, disabled }),
32051
- activeTab === "portals" && /* @__PURE__ */ jsx(TabPortals, { apiKey, prod, disabled })
33499
+ activeTab === "stock" && /* @__PURE__ */ jsx(
33500
+ TabStock,
33501
+ {
33502
+ apiKey,
33503
+ prod,
33504
+ disabled,
33505
+ onSelected: (file2) => {
33506
+ setActiveTab("upload");
33507
+ setFile(file2);
33508
+ setPreviewUrl(URL.createObjectURL(file2));
33509
+ }
33510
+ }
33511
+ ),
33512
+ activeTab === "portals" && /* @__PURE__ */ jsx(
33513
+ TabPortals,
33514
+ {
33515
+ apiKey,
33516
+ prod,
33517
+ disabled,
33518
+ onSelected: (file2) => {
33519
+ setActiveTab("upload");
33520
+ setFile(file2);
33521
+ setPreviewUrl(URL.createObjectURL(file2));
33522
+ }
33523
+ }
33524
+ )
32052
33525
  ] })
32053
33526
  ] });
32054
33527
  }
@@ -34551,150 +36024,69 @@ CropperImage.$define();
34551
36024
  CropperSelection.$define();
34552
36025
  CropperShade.$define();
34553
36026
  CropperViewer.$define();
34554
- function adaptAssetsListFromV2(v2Response) {
34555
- if (!v2Response?.success || !Array.isArray(v2Response?.data)) {
34556
- return { result: [] };
34557
- }
34558
- const legacyAssets = v2Response.data.map((asset) => {
34559
- return {
34560
- id: asset.id,
34561
- filename: asset.filename || asset.original_filename,
34562
- mime_type: asset.mime_type,
34563
- file_size: asset.file_size,
34564
- width: asset.width,
34565
- height: asset.height,
34566
- upload_date: asset.upload_date || asset.created_at,
34567
- processing_status: asset.processing_status || asset.status,
34568
- // URL principal
34569
- url: asset.master_url || asset.master?.url_signed,
34570
- webp_available: asset.webp_available || !!asset.webp_url,
34571
- // Solo metadatos básicos en listado
34572
- metadata: asset.metadata || {},
34573
- variants_count: asset.variants_count || 0
34574
- };
34575
- });
34576
- return { result: legacyAssets };
34577
- }
34578
- function adaptUploadFromV2(v2Response) {
34579
- if (!v2Response?.success || !v2Response?.data) {
34580
- return { result: null };
34581
- }
34582
- const asset = v2Response.data;
34583
- const legacyResponse = {
34584
- id: asset.id,
34585
- filename: asset.original_filename,
34586
- mime_type: asset.mime_type,
34587
- file_size: asset.file_size,
34588
- width: asset.width,
34589
- height: asset.height,
34590
- status: asset.status,
34591
- upload_date: asset.created_at,
34592
- // URL del master
34593
- url: asset.master?.url_signed,
34594
- master_format: asset.master?.format,
34595
- // Estado de procesamiento
34596
- processing: asset.processing || {
34597
- master_webp: asset.status === "processing" ? "queued" : "completed",
34598
- variants: asset.status === "processing" ? "queued" : "completed"
34599
- },
34600
- // Información adicional
34601
- checksum: asset.checksum,
34602
- storage_path: asset.storage_path_base,
34603
- metadata: asset.metadata || {}
34604
- };
34605
- return { result: legacyResponse };
34606
- }
34607
- function adaptErrorFromV2(error) {
34608
- if (error.message && !error.message.includes("API Error:")) {
34609
- return error;
34610
- }
34611
- let errorMessage = error.message || "Unknown API error";
34612
- if (errorMessage.startsWith("API Error: ")) {
34613
- errorMessage = errorMessage.substring(11);
34614
- }
34615
- return new Error(errorMessage);
34616
- }
34617
- const BASE_PATH = "/api/v2";
34618
- async function listAssets(params = {}) {
34619
- try {
34620
- const queryString = new URLSearchParams(params).toString();
34621
- const endpoint = queryString ? `/assets?${queryString}` : "/assets";
34622
- const response = await callApi({
34623
- endpoint,
34624
- method: "GET",
34625
- basePath: BASE_PATH,
34626
- useJWT: true
34627
- });
34628
- return adaptAssetsListFromV2(response);
34629
- } catch (error) {
34630
- throw adaptErrorFromV2(error);
34631
- }
34632
- }
34633
- async function uploadAsset(file, uploaded_by = null, store_original = false) {
34634
- try {
34635
- const formData = new FormData();
34636
- formData.append("file", file);
34637
- if (uploaded_by) formData.append("uploaded_by", uploaded_by);
34638
- if (store_original) formData.append("store_original", store_original.toString());
34639
- const response = await callApi({
34640
- endpoint: "/assets",
34641
- method: "POST",
34642
- body: formData,
34643
- basePath: BASE_PATH,
34644
- isFormData: true,
34645
- useJWT: true
34646
- });
34647
- return adaptUploadFromV2(response);
34648
- } catch (error) {
34649
- throw adaptErrorFromV2(error);
34650
- }
34651
- }
34652
- async function deleteAsset(assetId) {
34653
- try {
34654
- const response = await callApi({
34655
- endpoint: `/assets/${assetId}`,
34656
- method: "DELETE",
34657
- basePath: BASE_PATH,
34658
- useJWT: true
34659
- });
34660
- return response;
34661
- } catch (error) {
34662
- throw adaptErrorFromV2(error);
34663
- }
34664
- }
34665
- function useUploadImage() {
36027
+ function useCreateVariant() {
34666
36028
  const [loading, setLoading] = useState(false);
34667
36029
  const [error, setError] = useState(null);
34668
- const [uploadedImage, setUploadedImage] = useState(null);
34669
- const upload = async (file, uploadedBy = null) => {
34670
- if (!file) {
34671
- setError("No se ha proporcionado ningún archivo");
36030
+ const [createdVariant, setCreatedVariant] = useState(null);
36031
+ const createVariant = async (assetId, variantConfig) => {
36032
+ if (!assetId || !variantConfig) {
36033
+ setError("ID de asset y configuración de variante son requeridos");
34672
36034
  return null;
34673
36035
  }
34674
36036
  setLoading(true);
34675
36037
  setError(null);
34676
- setUploadedImage(null);
36038
+ setCreatedVariant(null);
34677
36039
  try {
34678
- const data = await uploadAsset(file, uploadedBy, false);
34679
- const result = data.result || data;
34680
- setUploadedImage(result);
34681
- return result;
36040
+ const response = await generateVariant(assetId, {
36041
+ variant_name: variantConfig.name,
36042
+ width: variantConfig.width,
36043
+ height: variantConfig.height,
36044
+ crop_params: variantConfig.crop_params,
36045
+ preset_aspect: variantConfig.preset_aspect,
36046
+ preset_size: variantConfig.preset_size,
36047
+ output_format: variantConfig.output_format
36048
+ });
36049
+ if (response?.result) {
36050
+ setCreatedVariant(response.result);
36051
+ return response.result;
36052
+ } else {
36053
+ throw new Error("No se pudo crear la variante");
36054
+ }
34682
36055
  } catch (err) {
34683
- setError(err.message);
36056
+ console.error("Error creating variant:", err);
36057
+ setError(err.message || "Error desconocido al crear variante");
34684
36058
  return null;
34685
36059
  } finally {
34686
36060
  setLoading(false);
34687
36061
  }
34688
36062
  };
36063
+ const createCropVariant = async (assetId, cropData, options = {}) => {
36064
+ const variantConfig = {
36065
+ name: options.name || `crop_${Date.now()}`,
36066
+ width: options.width || 800,
36067
+ // Usar las dimensiones calculadas de options
36068
+ height: options.height || 600,
36069
+ // Usar las dimensiones calculadas de options
36070
+ output_format: options.format || "webp",
36071
+ crop_params: {
36072
+ x: cropData.x || 0,
36073
+ y: cropData.y || 0,
36074
+ width: cropData.width || 1,
36075
+ height: cropData.height || 1
36076
+ }
36077
+ };
36078
+ return await createVariant(assetId, variantConfig);
36079
+ };
34689
36080
  const reset = () => {
34690
36081
  setError(null);
34691
- setUploadedImage(null);
36082
+ setCreatedVariant(null);
34692
36083
  };
34693
36084
  return {
34694
- upload,
36085
+ createVariant,
36086
+ createCropVariant,
34695
36087
  loading,
34696
36088
  error,
34697
- uploadedImage,
36089
+ createdVariant,
34698
36090
  reset
34699
36091
  };
34700
36092
  }
@@ -35406,8 +36798,8 @@ function CropperView({
35406
36798
  onCancel,
35407
36799
  onDelete,
35408
36800
  deleting = false,
35409
- apiKey,
35410
- prod = false
36801
+ onVariantCreated = null
36802
+ // Callback cuando se crea una variante
35411
36803
  }) {
35412
36804
  const [showPreview, setShowPreview] = useState(false);
35413
36805
  const [previewUrl, setPreviewUrl] = useState(null);
@@ -35435,10 +36827,10 @@ function CropperView({
35435
36827
  ];
35436
36828
  }, []);
35437
36829
  const {
35438
- upload,
35439
- loading: uploading,
35440
- error: uploadError
35441
- } = useUploadImage();
36830
+ createCropVariant,
36831
+ loading: creatingVariant,
36832
+ error: variantError
36833
+ } = useCreateVariant();
35442
36834
  const cropper = useCropper(image, {
35443
36835
  aspectRatio,
35444
36836
  showGrid,
@@ -35449,6 +36841,13 @@ function CropperView({
35449
36841
  const { refs, state, transform, selection, utils } = cropper;
35450
36842
  const { canvasRef, imageRef, selectionRef } = refs;
35451
36843
  const { cropData, imageInfo, canExport, transformVersion } = state;
36844
+ const effectiveImageInfo = useMemo(() => imageInfo || {
36845
+ naturalWidth: image.width || 1920,
36846
+ // Usar dimensiones de la imagen prop como fallback
36847
+ naturalHeight: image.height || 1080,
36848
+ currentWidth: image.width || 1920,
36849
+ currentHeight: image.height || 1080
36850
+ }, [imageInfo, image.width, image.height]);
35452
36851
  const toggleGrid = useCallback(() => setShowGrid((v) => !v), []);
35453
36852
  const toggleShade = useCallback(() => setShade((v) => !v), []);
35454
36853
  const toggleTips = useCallback(() => setShowTips((v) => !v), []);
@@ -35550,50 +36949,79 @@ function CropperView({
35550
36949
  alert(errorMsg);
35551
36950
  return;
35552
36951
  }
35553
- accessibilityManager?.announce("Guardando imagen recortada");
36952
+ if (!state.isReady) {
36953
+ const errorMsg = "El cropper aún no está inicializado. Espera un momento e inténtalo de nuevo.";
36954
+ accessibilityManager?.announceError(errorMsg);
36955
+ alert(errorMsg);
36956
+ return;
36957
+ }
36958
+ accessibilityManager?.announce("Creando variante recortada de la imagen");
35554
36959
  try {
35555
- const canvas = await selection.toCanvas({
35556
- width: 800,
35557
- height: 600,
35558
- imageSmoothingEnabled: true,
35559
- imageSmoothingQuality: "high"
36960
+ if (!cropData || !effectiveImageInfo) {
36961
+ console.error("❌ Datos faltantes:", { cropData, effectiveImageInfo });
36962
+ throw new Error(`No hay datos de recorte disponibles. CropData: ${!!cropData}, ImageInfo: ${!!effectiveImageInfo}`);
36963
+ }
36964
+ if (!cropData.x && cropData.x !== 0 || !cropData.y && cropData.y !== 0 || !cropData.width || !cropData.height) {
36965
+ console.error("❌ CropData inválido:", cropData);
36966
+ throw new Error("Los datos de recorte no tienen las propiedades esperadas");
36967
+ }
36968
+ if (!effectiveImageInfo.naturalWidth || !effectiveImageInfo.naturalHeight) {
36969
+ console.error("❌ ImageInfo inválido:", effectiveImageInfo);
36970
+ throw new Error("Los datos de imagen no tienen las dimensiones esperadas");
36971
+ }
36972
+ const { x, y, width, height } = cropData;
36973
+ const { naturalWidth, naturalHeight } = effectiveImageInfo;
36974
+ const cropParams = {
36975
+ x: x / naturalWidth,
36976
+ y: y / naturalHeight,
36977
+ width: width / naturalWidth,
36978
+ height: height / naturalHeight
36979
+ };
36980
+ const cropAspectRatio = width / height;
36981
+ let variantWidth, variantHeight;
36982
+ const maxDimension = 1200;
36983
+ if (cropAspectRatio > 1) {
36984
+ variantWidth = Math.min(width, maxDimension);
36985
+ variantHeight = Math.round(variantWidth / cropAspectRatio);
36986
+ } else {
36987
+ variantHeight = Math.min(height, maxDimension);
36988
+ variantWidth = Math.round(variantHeight * cropAspectRatio);
36989
+ }
36990
+ const ts = Date.now();
36991
+ const [name] = image.filename.split(".");
36992
+ const variantName = `${name}_crop_${ts}`;
36993
+ const result = await createCropVariant(image.id, cropParams, {
36994
+ name: variantName,
36995
+ width: variantWidth,
36996
+ height: variantHeight,
36997
+ format: "webp",
36998
+ quality: 90
35560
36999
  });
35561
- if (!canvas) return;
35562
- canvas.toBlob(
35563
- async (blob) => {
35564
- if (!blob) return;
35565
- const ts = Date.now();
35566
- const [name, ...rest] = image.filename.split(".");
35567
- const ext = rest.pop() || "jpg";
35568
- const newName = `${name}_cropped_${ts}.${ext}`;
35569
- const file = new File([blob], newName, {
35570
- type: blob.type || "image/jpeg"
35571
- });
35572
- const result = await upload(file, `cropped_from_${image.id}`);
35573
- if (result) {
35574
- accessibilityManager?.announceSuccess(
35575
- `Imagen recortada guardada como ${newName}`
35576
- );
35577
- onSave(result);
35578
- }
35579
- },
35580
- "image/jpeg",
35581
- 0.9
35582
- );
37000
+ if (result) {
37001
+ accessibilityManager?.announceSuccess(
37002
+ `Variante recortada creada: ${variantName}`
37003
+ );
37004
+ onVariantCreated?.(image.id, result);
37005
+ onSave(result);
37006
+ }
35583
37007
  } catch (error) {
35584
- console.warn("Error saving crop:", error);
35585
- const errorMsg = "No se puede exportar el recorte por restricciones de CORS en la imagen original.";
37008
+ console.warn("Error creating crop variant:", error);
37009
+ const errorMsg = "No se pudo crear la variante recortada. Inténtalo de nuevo.";
35586
37010
  accessibilityManager?.announceError(errorMsg);
35587
37011
  alert(errorMsg);
35588
37012
  }
35589
37013
  }, [
35590
37014
  canExport,
37015
+ cropData,
37016
+ imageInfo,
37017
+ effectiveImageInfo,
35591
37018
  image.filename,
35592
37019
  image.id,
35593
- upload,
37020
+ createCropVariant,
35594
37021
  onSave,
35595
- selection,
35596
- accessibilityManager
37022
+ onVariantCreated,
37023
+ accessibilityManager,
37024
+ state.isReady
35597
37025
  ]);
35598
37026
  useEffect(() => {
35599
37027
  setShowPreview(false);
@@ -35691,7 +37119,7 @@ function CropperView({
35691
37119
  "button",
35692
37120
  {
35693
37121
  onClick: onCancel,
35694
- disabled: uploading,
37122
+ disabled: creatingVariant,
35695
37123
  className: "limbo-btn limbo-btn-secondary px-4 py-2 flex-1 sm:flex-initial",
35696
37124
  children: "Cancelar"
35697
37125
  }
@@ -35700,7 +37128,7 @@ function CropperView({
35700
37128
  "button",
35701
37129
  {
35702
37130
  onClick: () => onDelete?.(image),
35703
- disabled: deleting | uploading,
37131
+ disabled: deleting | creatingVariant,
35704
37132
  className: "limbo-btn limbo-btn-danger px-4 py-2 flex-1 sm:flex-initial",
35705
37133
  "aria-label": `Eliminar imagen ${image.filename}`,
35706
37134
  children: deleting ? "Eliminando..." : "Eliminar"
@@ -35709,10 +37137,10 @@ function CropperView({
35709
37137
  ] })
35710
37138
  ] }),
35711
37139
  /* @__PURE__ */ jsxs("div", { className: "limbo-cropper-status bg-white border-gray-100 px-4 sm:px-6 py-2 pb-0 flex-shrink-0", children: [
35712
- uploadError && /* @__PURE__ */ jsxs("div", { className: "alert alert-danger mb-2 text-sm", role: "alert", children: [
37140
+ variantError && /* @__PURE__ */ jsxs("div", { className: "alert alert-danger mb-2 text-sm", role: "alert", children: [
35713
37141
  /* @__PURE__ */ jsx("strong", { children: "Error:" }),
35714
37142
  " ",
35715
- uploadError
37143
+ variantError
35716
37144
  ] }),
35717
37145
  cropData && cropData.width > 0 && /* @__PURE__ */ jsxs("div", { className: "alert alert-info mb-2 text-sm", role: "status", children: [
35718
37146
  /* @__PURE__ */ jsx("strong", { children: "Área:" }),
@@ -35798,7 +37226,7 @@ function CropperView({
35798
37226
  "cropper-image",
35799
37227
  {
35800
37228
  ref: imageRef,
35801
- src: image.path,
37229
+ src: image.url || image.path,
35802
37230
  alt: image.filename,
35803
37231
  id: "limbo-cropperjs-image",
35804
37232
  action: "move",
@@ -35869,7 +37297,7 @@ function CropperView({
35869
37297
  "button",
35870
37298
  {
35871
37299
  onClick: preview,
35872
- disabled: uploading || !canExport,
37300
+ disabled: creatingVariant || !canExport,
35873
37301
  className: `w-full min-h-10 transition-colors ${showPreview ? "limbo-btn limbo-btn-danger" : "limbo-btn limbo-btn-primary"}`,
35874
37302
  "aria-label": "Generar vista previa del recorte",
35875
37303
  children: [
@@ -35887,10 +37315,10 @@ function CropperView({
35887
37315
  "button",
35888
37316
  {
35889
37317
  onClick: saveCrop,
35890
- disabled: uploading || !cropData || !canExport,
37318
+ disabled: creatingVariant || !cropData || !effectiveImageInfo || !canExport || !state.isReady,
35891
37319
  className: "w-full limbo-btn limbo-btn-success min-h-10",
35892
37320
  "aria-label": "Guardar imagen recortada",
35893
- children: uploading ? /* @__PURE__ */ jsxs(Fragment, { children: [
37321
+ children: creatingVariant ? /* @__PURE__ */ jsxs(Fragment, { children: [
35894
37322
  /* @__PURE__ */ jsx("span", { className: "icon icon-save-white" }),
35895
37323
  " ",
35896
37324
  "Guardando..."
@@ -35904,7 +37332,7 @@ function CropperView({
35904
37332
  "button",
35905
37333
  {
35906
37334
  onClick: resetAll,
35907
- disabled: uploading,
37335
+ disabled: creatingVariant,
35908
37336
  className: "w-full limbo-btn limbo-btn-secondary min-h-10",
35909
37337
  "aria-label": "Reiniciar todas las configuraciones",
35910
37338
  children: [
@@ -36057,7 +37485,7 @@ function CropperView({
36057
37485
  {
36058
37486
  onClick: () => handleAspectRatio(ratio.value),
36059
37487
  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"}`,
36060
- disabled: uploading,
37488
+ disabled: creatingVariant,
36061
37489
  title: `Cambiar a proporción ${ratio.label}`,
36062
37490
  children: ratio.label
36063
37491
  },
@@ -36069,7 +37497,7 @@ function CropperView({
36069
37497
  value: aspectRatio,
36070
37498
  onChange: (e) => handleAspectRatio(e.target.value),
36071
37499
  className: "w-full form-control hidden md:block",
36072
- disabled: uploading,
37500
+ disabled: creatingVariant,
36073
37501
  "aria-label": "Seleccionar proporción de aspecto",
36074
37502
  children: allowedAspectRatios.map((ratio) => /* @__PURE__ */ jsx("option", { value: ratio.value, children: ratio.label }, ratio.value))
36075
37503
  }
@@ -36083,7 +37511,7 @@ function CropperView({
36083
37511
  {
36084
37512
  onClick: toggleGrid,
36085
37513
  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"}`,
36086
- disabled: uploading,
37514
+ disabled: creatingVariant,
36087
37515
  "aria-pressed": showGrid,
36088
37516
  "aria-label": "Activar/desactivar grid",
36089
37517
  children: [
@@ -36104,7 +37532,7 @@ function CropperView({
36104
37532
  {
36105
37533
  onClick: toggleShade,
36106
37534
  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"}`,
36107
- disabled: uploading,
37535
+ disabled: creatingVariant,
36108
37536
  "aria-pressed": shade,
36109
37537
  "aria-label": "Activar/desactivar sombreado",
36110
37538
  children: [
@@ -36132,7 +37560,7 @@ function CropperView({
36132
37560
  onClick: centerSelection,
36133
37561
  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",
36134
37562
  title: "Centrar selección",
36135
- disabled: uploading,
37563
+ disabled: creatingVariant,
36136
37564
  "aria-label": "Centrar área de selección",
36137
37565
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
36138
37566
  /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button icon--sm align-[middle!important] " }),
@@ -36147,7 +37575,7 @@ function CropperView({
36147
37575
  onClick: resetSelection,
36148
37576
  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",
36149
37577
  title: "Reiniciar selección",
36150
- disabled: uploading,
37578
+ disabled: creatingVariant,
36151
37579
  "aria-label": "Reiniciar área de selección",
36152
37580
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
36153
37581
  /* @__PURE__ */ jsx("span", { className: "icon icon-refresh icon--sm lign-[middle!important] " }),
@@ -36165,7 +37593,7 @@ function CropperView({
36165
37593
  {
36166
37594
  onClick: () => setSelectionCoverage(0.5),
36167
37595
  className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36168
- disabled: uploading,
37596
+ disabled: creatingVariant,
36169
37597
  "aria-label": "Selección 50%",
36170
37598
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "50%" }) })
36171
37599
  }
@@ -36175,7 +37603,7 @@ function CropperView({
36175
37603
  {
36176
37604
  onClick: () => setSelectionCoverage(0.7),
36177
37605
  className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36178
- disabled: uploading,
37606
+ disabled: creatingVariant,
36179
37607
  "aria-label": "Selección 70%",
36180
37608
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "70%" }) })
36181
37609
  }
@@ -36185,7 +37613,7 @@ function CropperView({
36185
37613
  {
36186
37614
  onClick: () => setSelectionCoverage(0.9),
36187
37615
  className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36188
- disabled: uploading,
37616
+ disabled: creatingVariant,
36189
37617
  "aria-label": "Selección 90%",
36190
37618
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "90%" }) })
36191
37619
  }
@@ -36195,7 +37623,7 @@ function CropperView({
36195
37623
  {
36196
37624
  onClick: () => setSelectionCoverage(1),
36197
37625
  className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36198
- disabled: uploading,
37626
+ disabled: creatingVariant,
36199
37627
  "aria-label": "Selección completa",
36200
37628
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "100%" }) })
36201
37629
  }
@@ -36215,7 +37643,7 @@ function CropperView({
36215
37643
  onClick: () => move(0, -10),
36216
37644
  className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36217
37645
  title: "Mover arriba",
36218
- disabled: uploading,
37646
+ disabled: creatingVariant,
36219
37647
  "aria-label": "Mover imagen hacia arriba",
36220
37648
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-up-blue" }) })
36221
37649
  }
@@ -36227,7 +37655,7 @@ function CropperView({
36227
37655
  onClick: () => move(-10, 0),
36228
37656
  className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36229
37657
  title: "Mover izquierda",
36230
- disabled: uploading,
37658
+ disabled: creatingVariant,
36231
37659
  "aria-label": "Mover imagen hacia la izquierda",
36232
37660
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-blue" }) })
36233
37661
  }
@@ -36238,7 +37666,7 @@ function CropperView({
36238
37666
  onClick: centerImage,
36239
37667
  className: "p-2 rounded cursor-pointer transition-colors text-xs bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36240
37668
  title: "Centrar y ajustar imagen",
36241
- disabled: uploading,
37669
+ disabled: creatingVariant,
36242
37670
  "aria-label": "Centrar y ajustar imagen",
36243
37671
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-radio-button-blue icon-md" }) })
36244
37672
  }
@@ -36249,7 +37677,7 @@ function CropperView({
36249
37677
  onClick: () => move(10, 0),
36250
37678
  className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36251
37679
  title: "Mover derecha",
36252
- disabled: uploading,
37680
+ disabled: creatingVariant,
36253
37681
  "aria-label": "Mover imagen hacia la derecha",
36254
37682
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-blue" }) })
36255
37683
  }
@@ -36261,7 +37689,7 @@ function CropperView({
36261
37689
  onClick: () => move(0, 10),
36262
37690
  className: "p-2 rounded cursor-pointer transition-colors text-sm bg-gray-100 border border-gray-300 text-gray-700 hover:bg-gray-200",
36263
37691
  title: "Mover abajo",
36264
- disabled: uploading,
37692
+ disabled: creatingVariant,
36265
37693
  "aria-label": "Mover imagen hacia abajo",
36266
37694
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-down-blue" }) })
36267
37695
  }
@@ -36281,7 +37709,7 @@ function CropperView({
36281
37709
  onClick: () => zoom(-0.2),
36282
37710
  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",
36283
37711
  title: "Alejar 20%",
36284
- disabled: uploading,
37712
+ disabled: creatingVariant,
36285
37713
  "aria-label": "Alejar imagen",
36286
37714
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-out-blue" }) })
36287
37715
  }
@@ -36292,7 +37720,7 @@ function CropperView({
36292
37720
  onClick: resetZoomOnly,
36293
37721
  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",
36294
37722
  title: "Restablecer zoom original",
36295
- disabled: uploading,
37723
+ disabled: creatingVariant,
36296
37724
  "aria-label": "Restablecer el zoom para que la imagen se vea con su resolución original",
36297
37725
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-screenshot-blue" }) })
36298
37726
  }
@@ -36303,7 +37731,7 @@ function CropperView({
36303
37731
  onClick: () => zoom(0.2),
36304
37732
  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",
36305
37733
  title: "Acercar 20%",
36306
- disabled: uploading,
37734
+ disabled: creatingVariant,
36307
37735
  "aria-label": "Acercar imagen",
36308
37736
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-zoom-in-blue" }) })
36309
37737
  }
@@ -36319,7 +37747,7 @@ function CropperView({
36319
37747
  onClick: () => rotate(-90),
36320
37748
  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",
36321
37749
  title: "Rotar -90°",
36322
- disabled: uploading,
37750
+ disabled: creatingVariant,
36323
37751
  "aria-label": "Rotar imagen 90 grados a la izquierda",
36324
37752
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-reply-blue" }) })
36325
37753
  }
@@ -36330,7 +37758,7 @@ function CropperView({
36330
37758
  onClick: () => rotate(-45),
36331
37759
  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",
36332
37760
  title: "Rotar -45°",
36333
- disabled: uploading,
37761
+ disabled: creatingVariant,
36334
37762
  "aria-label": "Rotar imagen 45 grados a la izquierda",
36335
37763
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "-45°" }) })
36336
37764
  }
@@ -36341,7 +37769,7 @@ function CropperView({
36341
37769
  onClick: () => rotate(45),
36342
37770
  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",
36343
37771
  title: "Rotar +45°",
36344
- disabled: uploading,
37772
+ disabled: creatingVariant,
36345
37773
  "aria-label": "Rotar imagen 45 grados a la derecha",
36346
37774
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { children: "+45°" }) })
36347
37775
  }
@@ -36352,7 +37780,7 @@ function CropperView({
36352
37780
  onClick: () => rotate(90),
36353
37781
  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",
36354
37782
  title: "Rotar +90°",
36355
- disabled: uploading,
37783
+ disabled: creatingVariant,
36356
37784
  "aria-label": "Rotar imagen 90 grados a la derecha",
36357
37785
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "icon icon-send-arrow-blue" }) })
36358
37786
  }
@@ -36368,7 +37796,7 @@ function CropperView({
36368
37796
  onClick: flipHorizontal,
36369
37797
  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"}`,
36370
37798
  title: `Voltear horizontalmente ${flipStates.horizontal ? "(activo)" : ""}`,
36371
- disabled: uploading,
37799
+ disabled: creatingVariant,
36372
37800
  "aria-label": "Voltear imagen horizontalmente",
36373
37801
  "aria-pressed": flipStates.horizontal,
36374
37802
  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" }) })
@@ -36380,7 +37808,7 @@ function CropperView({
36380
37808
  onClick: flipVertical,
36381
37809
  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"}`,
36382
37810
  title: `Voltear verticalmente ${flipStates.vertical ? "(activo)" : ""}`,
36383
- disabled: uploading,
37811
+ disabled: creatingVariant,
36384
37812
  "aria-label": "Voltear imagen verticalmente",
36385
37813
  "aria-pressed": flipStates.vertical,
36386
37814
  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" }) })
@@ -36397,7 +37825,7 @@ function CropperView({
36397
37825
  },
36398
37826
  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",
36399
37827
  title: "Reiniciar transformaciones",
36400
- disabled: uploading,
37828
+ disabled: creatingVariant,
36401
37829
  "aria-label": "Reiniciar todas las transformaciones de la imagen",
36402
37830
  children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs("span", { children: [
36403
37831
  /* @__PURE__ */ jsx("span", { className: "icon icon-refresh-blue icon--sm align-[middle!important] " }),
@@ -36411,6 +37839,187 @@ function CropperView({
36411
37839
  ] })
36412
37840
  ] });
36413
37841
  }
37842
+ function Pagination({
37843
+ currentPage,
37844
+ totalPages,
37845
+ onPageChange,
37846
+ disabled = false
37847
+ }) {
37848
+ if (!totalPages || totalPages <= 1) {
37849
+ return null;
37850
+ }
37851
+ const handlePrevious = () => {
37852
+ if (currentPage > 1) {
37853
+ onPageChange(currentPage - 1);
37854
+ }
37855
+ };
37856
+ const handleNext = () => {
37857
+ if (currentPage < totalPages) {
37858
+ onPageChange(currentPage + 1);
37859
+ }
37860
+ };
37861
+ const handlePageClick = (page) => {
37862
+ if (page !== currentPage) {
37863
+ onPageChange(page);
37864
+ }
37865
+ };
37866
+ const getVisiblePages = () => {
37867
+ const delta = 2;
37868
+ const range = [];
37869
+ const rangeWithDots = [];
37870
+ const start = Math.max(1, currentPage - delta);
37871
+ const end = Math.min(totalPages, currentPage + delta);
37872
+ for (let i = start; i <= end; i++) {
37873
+ range.push(i);
37874
+ }
37875
+ if (start > 1) {
37876
+ rangeWithDots.push(1);
37877
+ if (start > 2) {
37878
+ rangeWithDots.push("...");
37879
+ }
37880
+ }
37881
+ rangeWithDots.push(...range);
37882
+ if (end < totalPages) {
37883
+ if (end < totalPages - 1) {
37884
+ rangeWithDots.push("...");
37885
+ }
37886
+ rangeWithDots.push(totalPages);
37887
+ }
37888
+ return rangeWithDots;
37889
+ };
37890
+ const visiblePages = getVisiblePages();
37891
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center space-x-2 py-4", children: [
37892
+ /* @__PURE__ */ jsxs(
37893
+ "button",
37894
+ {
37895
+ onClick: handlePrevious,
37896
+ disabled: disabled || currentPage <= 1,
37897
+ className: `
37898
+ px-3 py-2 text-sm font-medium rounded-md transition-colors disabled:cursor-default cursor-pointer
37899
+ ${disabled || currentPage <= 1 ? "text-gray-400 bg-gray-100 cursor-not-allowed" : "text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:ring-2 focus:ring-blue-500"}
37900
+ `,
37901
+ children: [
37902
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-left-white icon--sm" }),
37903
+ " Anterior"
37904
+ ]
37905
+ }
37906
+ ),
37907
+ /* @__PURE__ */ jsx("div", { className: "flex space-x-1", children: visiblePages.map((page, index) => /* @__PURE__ */ jsx(React.Fragment, { children: page === "..." ? /* @__PURE__ */ jsx("span", { className: "px-3 py-2 text-sm text-gray-500", children: "..." }) : /* @__PURE__ */ jsx(
37908
+ "button",
37909
+ {
37910
+ onClick: () => handlePageClick(page),
37911
+ disabled,
37912
+ className: `
37913
+ px-3 py-2 text-sm font-medium rounded-md transition-colors disabled:cursor-default cursor-pointer
37914
+ ${page === currentPage ? "bg-blue-600 text-white border border-blue-600" : disabled ? "text-gray-400 bg-gray-100 cursor-not-allowed" : "text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:ring-2 focus:ring-blue-500"}
37915
+ `,
37916
+ children: page
37917
+ }
37918
+ ) }, `page-${page}-${index}`)) }),
37919
+ /* @__PURE__ */ jsxs(
37920
+ "button",
37921
+ {
37922
+ onClick: handleNext,
37923
+ disabled: disabled || currentPage >= totalPages,
37924
+ className: `
37925
+ px-3 py-2 text-sm font-medium rounded-md transition-colors disabled:cursor-default cursor-pointer
37926
+ ${disabled || currentPage >= totalPages ? "text-gray-400 bg-gray-100 cursor-not-allowed" : "text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:ring-2 focus:ring-blue-500"}
37927
+ `,
37928
+ children: [
37929
+ "Siguiente ",
37930
+ /* @__PURE__ */ jsx("span", { className: "icon icon-arrow-right-white icon--sm" })
37931
+ ]
37932
+ }
37933
+ ),
37934
+ /* @__PURE__ */ jsxs("div", { className: "text-sm text-gray-500 ml-4", children: [
37935
+ "Página ",
37936
+ currentPage,
37937
+ " de ",
37938
+ totalPages
37939
+ ] })
37940
+ ] });
37941
+ }
37942
+ function TokenExpiredModal({ isOpen, onClose }) {
37943
+ if (!isOpen) return null;
37944
+ return /* @__PURE__ */ jsx(
37945
+ "div",
37946
+ {
37947
+ className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50",
37948
+ style: { zIndex: 9999 },
37949
+ children: /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg p-6 max-w-md mx-4 shadow-xl", children: [
37950
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center mb-4", children: [
37951
+ /* @__PURE__ */ jsx(
37952
+ "svg",
37953
+ {
37954
+ className: "w-6 h-6 text-red-600 mr-3",
37955
+ fill: "none",
37956
+ stroke: "currentColor",
37957
+ viewBox: "0 0 24 24",
37958
+ children: /* @__PURE__ */ jsx(
37959
+ "path",
37960
+ {
37961
+ strokeLinecap: "round",
37962
+ strokeLinejoin: "round",
37963
+ strokeWidth: 2,
37964
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
37965
+ }
37966
+ )
37967
+ }
37968
+ ),
37969
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "Sesión Expirada" })
37970
+ ] }),
37971
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
37972
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 mb-2", children: "Su sesión ha expirado por seguridad." }),
37973
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600", children: "Es necesario recargar la página para obtener un nuevo token de acceso." })
37974
+ ] }),
37975
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end space-x-3", children: /* @__PURE__ */ jsx(
37976
+ "button",
37977
+ {
37978
+ onClick: onClose,
37979
+ className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors",
37980
+ children: "Recargar Página"
37981
+ }
37982
+ ) })
37983
+ ] })
37984
+ }
37985
+ );
37986
+ }
37987
+ function useUploadImage() {
37988
+ const [loading, setLoading] = useState(false);
37989
+ const [error, setError] = useState(null);
37990
+ const [uploadedImage, setUploadedImage] = useState(null);
37991
+ const upload = async (file, uploadedBy = null) => {
37992
+ if (!file) {
37993
+ setError("No se ha proporcionado ningún archivo");
37994
+ return null;
37995
+ }
37996
+ setLoading(true);
37997
+ setError(null);
37998
+ setUploadedImage(null);
37999
+ try {
38000
+ const data = await uploadAsset(file, uploadedBy, false);
38001
+ const result = data.result || data;
38002
+ setUploadedImage(result);
38003
+ return result;
38004
+ } catch (err) {
38005
+ setError(err.message);
38006
+ return null;
38007
+ } finally {
38008
+ setLoading(false);
38009
+ }
38010
+ };
38011
+ const reset = () => {
38012
+ setError(null);
38013
+ setUploadedImage(null);
38014
+ };
38015
+ return {
38016
+ upload,
38017
+ loading,
38018
+ error,
38019
+ uploadedImage,
38020
+ reset
38021
+ };
38022
+ }
36414
38023
  function useDeleteImage() {
36415
38024
  const [loading, setLoading] = useState(false);
36416
38025
  const [error, setError] = useState(null);
@@ -36447,29 +38056,48 @@ function useImages(apiKey, prod = false, params = {}) {
36447
38056
  const [images, setImages] = useState([]);
36448
38057
  const [loading, setLoading] = useState(true);
36449
38058
  const [error, setError] = useState(null);
38059
+ const [pagination, setPagination] = useState(null);
38060
+ const [retryCount, setRetryCount] = useState(0);
38061
+ const MAX_RETRIES = 3;
38062
+ const paramsKey = useMemo(() => JSON.stringify(params), [params]);
36450
38063
  useEffect(() => {
36451
- const paramsKey = JSON.stringify(params);
36452
38064
  const cacheKey = `${paramsKey}`;
36453
38065
  const cached = cache.get(cacheKey);
36454
38066
  const now = Date.now();
36455
38067
  if (cached && now - cached.timestamp < CACHE_TTL) {
36456
38068
  setImages(cached.data);
38069
+ setPagination(cached.pagination || null);
36457
38070
  setLoading(false);
36458
38071
  return;
36459
38072
  }
36460
38073
  let isMounted = true;
36461
38074
  const fetchImages = async () => {
38075
+ if (retryCount >= MAX_RETRIES) {
38076
+ if (isMounted) {
38077
+ setError(`Error de autenticación: Verifica tus credenciales API`);
38078
+ setLoading(false);
38079
+ }
38080
+ return;
38081
+ }
36462
38082
  try {
36463
38083
  const data = await listAssets(params);
36464
38084
  if (!isMounted) return;
36465
38085
  const result = data.result || [];
38086
+ const paginationData = data.pagination || null;
36466
38087
  setImages(result);
38088
+ setPagination(paginationData);
38089
+ setError(null);
38090
+ setRetryCount(0);
36467
38091
  cache.set(cacheKey, {
36468
38092
  data: result,
38093
+ pagination: paginationData,
36469
38094
  timestamp: now
36470
38095
  });
36471
38096
  } catch (err) {
36472
- if (isMounted) setError(err.message);
38097
+ if (isMounted) {
38098
+ setError(err.message);
38099
+ setRetryCount((prev) => prev + 1);
38100
+ }
36473
38101
  } finally {
36474
38102
  if (isMounted) setLoading(false);
36475
38103
  }
@@ -36478,12 +38106,45 @@ function useImages(apiKey, prod = false, params = {}) {
36478
38106
  return () => {
36479
38107
  isMounted = false;
36480
38108
  };
36481
- }, [apiKey, prod, params]);
38109
+ }, [apiKey, prod, paramsKey, retryCount, params]);
36482
38110
  const invalidateCache = () => {
36483
- const paramsKey = JSON.stringify(params);
36484
38111
  cache.delete(`${paramsKey}`);
36485
38112
  };
36486
- return { images, loading, error, setImages, invalidateCache };
38113
+ return { images, loading, error, pagination, setImages, invalidateCache };
38114
+ }
38115
+ function useTokenExpiration() {
38116
+ const [isTokenExpired, setIsTokenExpired] = useState(false);
38117
+ const checkTokenExpiration = useCallback((error) => {
38118
+ if (error?.response?.status === 401) {
38119
+ const errorData = error.response.data;
38120
+ if (errorData?.error === "token_expired" || errorData?.message?.includes("expired") || errorData?.message?.includes("caducado") || errorData?.error?.includes("expired")) {
38121
+ setIsTokenExpired(true);
38122
+ return true;
38123
+ }
38124
+ }
38125
+ return false;
38126
+ }, []);
38127
+ const handleTokenExpiredClose = useCallback(() => {
38128
+ setIsTokenExpired(false);
38129
+ alert("El token ha expirado. La página se recargará para obtener un nuevo token.");
38130
+ window.location.reload();
38131
+ }, []);
38132
+ useEffect(() => {
38133
+ const handleGlobalError = (event) => {
38134
+ if (event.detail && event.detail.error) {
38135
+ checkTokenExpiration(event.detail.error);
38136
+ }
38137
+ };
38138
+ window.addEventListener("tokenExpiredError", handleGlobalError);
38139
+ return () => {
38140
+ window.removeEventListener("tokenExpiredError", handleGlobalError);
38141
+ };
38142
+ }, [checkTokenExpiration]);
38143
+ return {
38144
+ isTokenExpired,
38145
+ checkTokenExpiration,
38146
+ handleTokenExpiredClose
38147
+ };
36487
38148
  }
36488
38149
  function App({
36489
38150
  apiKey,
@@ -36494,7 +38155,7 @@ function App({
36494
38155
  modeUI = "full",
36495
38156
  // full | gallery-only | upload-only | crop-only | ia-only
36496
38157
  ui = {
36497
- showActions: ["select", "download", "copy", "delete", "crop"],
38158
+ showActions: ["select", "download", "copy", "delete", "crop", "variants"],
36498
38159
  hideActions: [],
36499
38160
  theme: "light",
36500
38161
  language: "es",
@@ -36502,7 +38163,9 @@ function App({
36502
38163
  showTabs: true
36503
38164
  },
36504
38165
  callbacks = {},
36505
- instanceId = null
38166
+ instanceId = null,
38167
+ itemsPerPage = 10
38168
+ // Número de elementos por página
36506
38169
  }) {
36507
38170
  const getFilteredFeatures = () => {
36508
38171
  switch (modeUI) {
@@ -36529,6 +38192,14 @@ function App({
36529
38192
  };
36530
38193
  const [activeTab, setActiveTab] = useState(getInitialTab());
36531
38194
  const [selectedImage, setSelectedImage] = useState(null);
38195
+ const [currentPage, setCurrentPage] = useState(1);
38196
+ const {
38197
+ isTokenExpired,
38198
+ handleTokenExpiredClose
38199
+ } = useTokenExpiration();
38200
+ const handlePageChange = (newPage) => {
38201
+ setCurrentPage(newPage);
38202
+ };
36532
38203
  const {
36533
38204
  upload,
36534
38205
  loading: uploading,
@@ -36546,9 +38217,14 @@ function App({
36546
38217
  images,
36547
38218
  loading: loadingImages,
36548
38219
  error: imagesError,
38220
+ pagination,
36549
38221
  invalidateCache,
36550
38222
  setImages
36551
- } = useImages(apiKey, prod);
38223
+ } = useImages(apiKey, prod, { limit: itemsPerPage, page: currentPage });
38224
+ const { refreshVariants } = useImageVariants();
38225
+ const handleVariantCreated = (assetId, variantData) => {
38226
+ refreshVariants(assetId);
38227
+ };
36552
38228
  const isActionAllowed = (action) => {
36553
38229
  if (ui.hideActions?.includes(action)) return false;
36554
38230
  if (ui.showActions?.includes(action)) return true;
@@ -36569,6 +38245,7 @@ function App({
36569
38245
  const result = await upload(file);
36570
38246
  if (result) {
36571
38247
  invalidateCache();
38248
+ setCurrentPage(1);
36572
38249
  setImages((prev) => [result, ...prev]);
36573
38250
  setActiveTab("gallery");
36574
38251
  if (callbacks.onUpload) {
@@ -36591,6 +38268,7 @@ function App({
36591
38268
  const success = await deleteImg(imageId);
36592
38269
  if (success) {
36593
38270
  invalidateCache();
38271
+ setCurrentPage(1);
36594
38272
  setImages((prev) => prev.filter((img) => img.id !== imageId));
36595
38273
  if (selectedImage && selectedImage.id === imageId) {
36596
38274
  setSelectedImage(null);
@@ -36654,6 +38332,15 @@ function App({
36654
38332
  crop: isActionAllowed("crop")
36655
38333
  }
36656
38334
  }
38335
+ ),
38336
+ pagination && !loadingImages && /* @__PURE__ */ jsx(
38337
+ Pagination,
38338
+ {
38339
+ currentPage: pagination.page || currentPage,
38340
+ totalPages: pagination.pages || 1,
38341
+ onPageChange: handlePageChange,
38342
+ disabled: loadingImages || deleting
38343
+ }
36657
38344
  )
36658
38345
  ] }),
36659
38346
  activeTab === "upload" && /* @__PURE__ */ jsxs("div", { children: [
@@ -36680,11 +38367,8 @@ function App({
36680
38367
  CropperView,
36681
38368
  {
36682
38369
  image: selectedImage,
36683
- apiKey,
36684
- prod,
36685
- onSave: (newImage) => {
38370
+ onSave: () => {
36686
38371
  invalidateCache();
36687
- setImages((prev) => [newImage, ...prev]);
36688
38372
  setSelectedImage(null);
36689
38373
  setActiveTab("gallery");
36690
38374
  },
@@ -36693,7 +38377,15 @@ function App({
36693
38377
  setActiveTab("gallery");
36694
38378
  },
36695
38379
  onDelete: () => handleDelete(selectedImage?.id),
36696
- deleting
38380
+ deleting,
38381
+ onVariantCreated: handleVariantCreated
38382
+ }
38383
+ ),
38384
+ /* @__PURE__ */ jsx(
38385
+ TokenExpiredModal,
38386
+ {
38387
+ isOpen: isTokenExpired,
38388
+ onClose: handleTokenExpiredClose
36697
38389
  }
36698
38390
  )
36699
38391
  ] });
@@ -37104,8 +38796,16 @@ class LimboInstance {
37104
38796
  * Validar configuración
37105
38797
  */
37106
38798
  _validateConfig() {
37107
- if (!this.config.apiKey) {
37108
- throw new Error(`LimboInstance ${this.id}: apiKey is required`);
38799
+ const hasAuth = this.config.auth?.apiKey || this.config.auth?.publicKey || this.config.apiKey || this.config.publicKey;
38800
+ if (!hasAuth) {
38801
+ throw new Error(`LimboInstance ${this.id}: Authentication is required. Provide either auth.publicKey (recommended) or auth.apiKey (for server-side only)`);
38802
+ }
38803
+ if ((this.config.auth?.apiKey || this.config.apiKey) && typeof window !== "undefined") {
38804
+ console.warn(
38805
+ `⚠️ SECURITY WARNING: API Key detected in client-side code.
38806
+ This is a security risk! Use publicKey instead for client applications.
38807
+ API Keys should only be used in server-side environments.`
38808
+ );
37109
38809
  }
37110
38810
  if (!["embed", "modal", "button"].includes(this.config.mode)) {
37111
38811
  throw new Error(`LimboInstance ${this.id}: invalid mode ${this.config.mode}`);
@@ -39819,15 +41519,25 @@ class LimboCore {
39819
41519
  */
39820
41520
  configure(options) {
39821
41521
  this.config.setGlobal(options);
39822
- if (options.auth || options.apiKey) {
39823
- try {
39824
- configureJWTFromConfigManager(this.config);
39825
- } catch (error) {
39826
- console.warn("Failed to configure JWT authentication:", error);
39827
- }
39828
- }
41522
+ configureApiClient({
41523
+ publicKey: options.publicKey,
41524
+ apiKey: options.apiKey,
41525
+ // Solo para authMode: "dev"
41526
+ token: options.token,
41527
+ // Solo para authMode: "manual"
41528
+ authMode: options.authMode,
41529
+ // "session" | "dev" | "manual"
41530
+ prod: options.prod || false
41531
+ });
39829
41532
  return this.config;
39830
41533
  }
41534
+ /**
41535
+ * Establecer token JWT (generado por backend)
41536
+ */
41537
+ setToken(token) {
41538
+ setToken(token);
41539
+ return this;
41540
+ }
39831
41541
  /**
39832
41542
  * Crear instancia manual
39833
41543
  */
@@ -39990,18 +41700,21 @@ const Limbo = new LimboCore();
39990
41700
  if (typeof window !== "undefined") {
39991
41701
  window.Limbo = Limbo;
39992
41702
  }
39993
- const PUBLIC_KEY = "pk_b23851734fb7601883bf4301b129a82f";
41703
+ const PUBLIC_KEY = "pk_9fcfdd91a14755cc68d0e11a14269554";
39994
41704
  if (typeof window !== "undefined" && document.querySelector("#root")) {
39995
41705
  Limbo.configure({
39996
- apiKey: PUBLIC_KEY,
39997
- prod: false
41706
+ prod: false,
41707
+ publicKey: PUBLIC_KEY,
41708
+ authMode: "session",
41709
+ tokenEndpoint: "/auth/token"
41710
+ // Endpoint que acepta solo public_key
39998
41711
  });
39999
41712
  Limbo.create({
40000
41713
  container: "#root",
40001
41714
  mode: "embed",
40002
41715
  modeUI: "full",
40003
41716
  features: ["gallery", "upload", "cropper"],
40004
- title: "Limbo Image Manager - Desarrollo",
41717
+ title: "Limbo Image Manager - Development",
40005
41718
  url: true
40006
41719
  });
40007
41720
  Limbo.configureAutoInputs({