@snowcone-app/ui 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5571,6 +5571,67 @@ function useMockupBackgroundOptional() {
5571
5571
  return useContext5(MockupBackgroundContext);
5572
5572
  }
5573
5573
 
5574
+ // src/patterns/croppedFrameComposite.ts
5575
+ var COMPOSITE_QUALITY = 0.92;
5576
+ function isFullRect(rect) {
5577
+ if (!rect) return true;
5578
+ return rect.x === 0 && rect.y === 0 && rect.w === 1 && rect.h === 1;
5579
+ }
5580
+ function compositeOffset(rect, bgWidth, bgHeight) {
5581
+ return {
5582
+ dx: Math.round(rect.x * bgWidth),
5583
+ dy: Math.round(rect.y * bgHeight)
5584
+ };
5585
+ }
5586
+ function allBackgroundsSeeded(mockupIds, seeded) {
5587
+ return mockupIds.length > 0 && mockupIds.every((id) => seeded.has(id));
5588
+ }
5589
+ function loadImageElement(url) {
5590
+ return new Promise((resolve, reject) => {
5591
+ const img = new Image();
5592
+ img.crossOrigin = "anonymous";
5593
+ img.onload = () => {
5594
+ if (img.decode) {
5595
+ img.decode().then(
5596
+ () => resolve(img),
5597
+ () => resolve(img)
5598
+ );
5599
+ } else {
5600
+ resolve(img);
5601
+ }
5602
+ };
5603
+ img.onerror = () => reject(
5604
+ new Error(`E_OVERLAY_FRAME_LOAD: image failed to load: ${url.slice(0, 160)}`)
5605
+ );
5606
+ img.src = url;
5607
+ });
5608
+ }
5609
+ async function compositeCroppedFrame(args) {
5610
+ const frame = await loadImageElement(args.frameUrl);
5611
+ const width = args.background.naturalWidth;
5612
+ const height = args.background.naturalHeight;
5613
+ if (!width || !height) {
5614
+ throw new Error("E_OVERLAY_BG_EMPTY: background image has no pixels");
5615
+ }
5616
+ const canvas = args.canvas ?? document.createElement("canvas");
5617
+ canvas.width = width;
5618
+ canvas.height = height;
5619
+ const ctx = canvas.getContext("2d");
5620
+ if (!ctx) {
5621
+ throw new Error("E_OVERLAY_NO_2D_CONTEXT: canvas 2d context unavailable");
5622
+ }
5623
+ ctx.drawImage(args.background, 0, 0);
5624
+ const { dx, dy } = compositeOffset(args.rect, width, height);
5625
+ ctx.drawImage(frame, dx, dy);
5626
+ const blob = await new Promise(
5627
+ (resolve) => canvas.toBlob(resolve, "image/webp", COMPOSITE_QUALITY)
5628
+ );
5629
+ if (!blob) {
5630
+ throw new Error("E_OVERLAY_ENCODE_FAILED: canvas.toBlob returned null");
5631
+ }
5632
+ return blob;
5633
+ }
5634
+
5574
5635
  // src/patterns/RealtimeProvider.tsx
5575
5636
  import { jsx as jsx52 } from "react/jsx-runtime";
5576
5637
  var DEFAULT_ARTBOARD_SIZE = 2e3;
@@ -5586,7 +5647,8 @@ function RealtimeProvider({
5586
5647
  mockupWidth = 1400,
5587
5648
  autoEnable = false,
5588
5649
  shop,
5589
- grantUrl
5650
+ grantUrl,
5651
+ delivery = "cropped"
5590
5652
  }) {
5591
5653
  const productContext = useProductOptional();
5592
5654
  const mockupBg = useMockupBackgroundOptional();
@@ -5690,58 +5752,217 @@ function RealtimeProvider({
5690
5752
  const registeredBlobUrlsRef = useRef12(/* @__PURE__ */ new Set());
5691
5753
  const pendingPlacementsRef = useRef12(/* @__PURE__ */ new Set());
5692
5754
  const pendingMockupIdsRef = useRef12(/* @__PURE__ */ new Set());
5693
- useEffect17(() => {
5694
- if (rawMockupResults.length === 0) {
5695
- if (mockupResultsRef.current.length > 0) {
5696
- mockupResultsRef.current = [];
5697
- mockupSubscribersRef.current.forEach((cb) => cb([]));
5755
+ const backgroundsRef = useRef12(/* @__PURE__ */ new Map());
5756
+ const croppedActiveRef = useRef12(false);
5757
+ const cropCapableRef = useRef12(true);
5758
+ const lastConfigBaseRef = useRef12(null);
5759
+ const sceneGenRef = useRef12(0);
5760
+ const processedFrameRef = useRef12(/* @__PURE__ */ new Map());
5761
+ const compositeQueueRef = useRef12(/* @__PURE__ */ new Map());
5762
+ const compositeBusyRef = useRef12(/* @__PURE__ */ new Set());
5763
+ const compositeCanvasRef = useRef12(null);
5764
+ const registerBlobUrl = useCallback10((blobOrUrl, placementName) => {
5765
+ let url;
5766
+ if (blobOrUrl instanceof Blob) {
5767
+ if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
5768
+ const oldest = registeredBlobUrlsRef.current.values().next().value;
5769
+ if (oldest) {
5770
+ URL.revokeObjectURL(oldest);
5771
+ registeredBlobUrlsRef.current.delete(oldest);
5772
+ mockupUrlCacheRef.current.forEach((value, key) => {
5773
+ if (value.url === oldest) {
5774
+ mockupUrlCacheRef.current.delete(key);
5775
+ }
5776
+ });
5777
+ mockupResultsRef.current = mockupResultsRef.current.filter(
5778
+ (r) => r.imageUrl !== oldest
5779
+ );
5780
+ }
5698
5781
  }
5699
- return;
5782
+ url = URL.createObjectURL(blobOrUrl);
5783
+ registeredBlobUrlsRef.current.add(url);
5784
+ } else {
5785
+ url = blobOrUrl;
5700
5786
  }
5701
- console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}`));
5702
- mockupResultsRef.current = rawMockupResults;
5703
- const now = Date.now();
5704
- rawMockupResults.forEach((result) => {
5705
- if (result.imageUrl) {
5787
+ return url;
5788
+ }, []);
5789
+ const croppedDeliveryEnabled = delivery !== "full-frame";
5790
+ const sendCropConfig = useCallback10(
5791
+ (withCrop) => {
5792
+ const base = lastConfigBaseRef.current;
5793
+ if (!base) return;
5794
+ croppedActiveRef.current = withCrop;
5795
+ sendConfigRaw({
5796
+ ...base,
5797
+ placementSettings: { ...placementSettingsOverridesRef.current },
5798
+ ...withCrop ? { overlayCrop: "auto" } : {}
5799
+ });
5800
+ },
5801
+ [sendConfigRaw]
5802
+ );
5803
+ const unflipCrop = useCallback10(() => {
5804
+ if (!croppedActiveRef.current) return;
5805
+ sendCropConfig(false);
5806
+ }, [sendCropConfig]);
5807
+ const maybeFlipCrop = useCallback10(() => {
5808
+ if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
5809
+ if (croppedActiveRef.current) return;
5810
+ const base = lastConfigBaseRef.current;
5811
+ if (!base) return;
5812
+ const ids = base.mockupIds ?? [];
5813
+ const seeded = new Set(backgroundsRef.current.keys());
5814
+ if (allBackgroundsSeeded(ids, seeded)) {
5815
+ console.log(`[RTP] Cropped delivery ON \u2014 all ${ids.length} mockup background(s) seeded`);
5816
+ sendCropConfig(true);
5817
+ } else {
5818
+ const missing = ids.filter((id) => !seeded.has(id));
5819
+ console.log(`[RTP] Cropped delivery waiting \u2014 ${missing.length}/${ids.length} mockup(s) unseeded: [${missing.slice(0, 8).join(", ")}]`);
5820
+ }
5821
+ }, [croppedDeliveryEnabled, sendCropConfig]);
5822
+ const invalidateBackgrounds = useCallback10(() => {
5823
+ sceneGenRef.current += 1;
5824
+ backgroundsRef.current.clear();
5825
+ processedFrameRef.current.clear();
5826
+ compositeQueueRef.current.clear();
5827
+ croppedActiveRef.current = false;
5828
+ }, []);
5829
+ const deliverResults = useCallback10(
5830
+ (results) => {
5831
+ if (results.length === 0) return;
5832
+ const now = Date.now();
5833
+ const merged = [...mockupResultsRef.current];
5834
+ results.forEach((result) => {
5835
+ const idx = merged.findIndex((r) => r.mockupId === result.mockupId);
5836
+ if (idx >= 0) merged[idx] = result;
5837
+ else merged.push(result);
5838
+ });
5839
+ mockupResultsRef.current = merged;
5840
+ const clearedPlacements = /* @__PURE__ */ new Set();
5841
+ results.forEach((result) => {
5706
5842
  const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
5707
5843
  mockupUrlCacheRef.current.set(result.mockupId, {
5708
5844
  url: result.imageUrl,
5709
- isBlob: false,
5710
- // Server URLs are not blob URLs
5845
+ isBlob: result.imageUrl.startsWith("blob:"),
5711
5846
  timestamp: now
5712
5847
  });
5713
5848
  pendingMockupIdsRef.current.delete(result.mockupId);
5714
5849
  if (wasPending) {
5715
5850
  console.log(`[RTP] Cleared pending for ${result.mockupId} \u2014 url cached`);
5716
5851
  }
5852
+ if (result.placement) clearedPlacements.add(result.placement);
5853
+ });
5854
+ if (clearedPlacements.size > 0) {
5855
+ clearedPlacements.forEach((p) => pendingPlacementsRef.current.delete(p));
5717
5856
  } else {
5718
- console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
5857
+ pendingPlacementsRef.current.clear();
5719
5858
  }
5720
- });
5721
- const clearedPlacements = /* @__PURE__ */ new Set();
5722
- rawMockupResults.forEach((result) => {
5723
- if (result.placement) clearedPlacements.add(result.placement);
5724
- });
5725
- if (clearedPlacements.size > 0) {
5726
- clearedPlacements.forEach((p) => pendingPlacementsRef.current.delete(p));
5727
- } else {
5728
- pendingPlacementsRef.current.clear();
5859
+ mockupSubscribersRef.current.forEach((cb) => cb(results));
5860
+ results.forEach((result) => {
5861
+ const subscribers = mockupSubscribersByIdRef.current.get(result.mockupId);
5862
+ if (subscribers && subscribers.size > 0) {
5863
+ console.log(`[RTP] Notifying ${subscribers.size} subscriber(s) for ${result.mockupId}`);
5864
+ subscribers.forEach((cb) => cb(result));
5865
+ } else {
5866
+ console.log(`[RTP] No subscribers for ${result.mockupId} \u2014 result received but nobody listening`);
5867
+ }
5868
+ });
5869
+ if (hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
5870
+ hasBlobInFlightRef.current = false;
5871
+ pipelineSettledSubscribersRef.current.forEach((cb) => cb());
5872
+ }
5873
+ },
5874
+ [hasPendingBlobs]
5875
+ );
5876
+ const runComposite = useCallback10(
5877
+ async (result) => {
5878
+ const gen = sceneGenRef.current;
5879
+ const bg = backgroundsRef.current.get(result.mockupId);
5880
+ if (!bg) {
5881
+ console.error(
5882
+ `[RealtimeProvider] E_OVERLAY_NO_BACKGROUND: cropped frame for ${result.mockupId} has no frozen background \u2014 falling back to full-frame and re-rendering it.`
5883
+ );
5884
+ unflipCrop();
5885
+ updateMockupIdsRaw([result.mockupId]);
5886
+ return;
5887
+ }
5888
+ try {
5889
+ if (!bg.imgPromise) bg.imgPromise = loadImageElement(bg.url);
5890
+ const background = await bg.imgPromise;
5891
+ if (!compositeCanvasRef.current && typeof document !== "undefined") {
5892
+ compositeCanvasRef.current = document.createElement("canvas");
5893
+ }
5894
+ const blob = await compositeCroppedFrame({
5895
+ background,
5896
+ frameUrl: result.imageUrl,
5897
+ rect: result.cropRect,
5898
+ canvas: compositeCanvasRef.current ?? void 0
5899
+ });
5900
+ if (gen !== sceneGenRef.current) return;
5901
+ deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
5902
+ } catch (err) {
5903
+ console.error(
5904
+ "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
5905
+ err
5906
+ );
5907
+ cropCapableRef.current = false;
5908
+ unflipCrop();
5909
+ updateMockupIdsRaw([result.mockupId]);
5910
+ }
5911
+ },
5912
+ [deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
5913
+ );
5914
+ const pumpCompositeQueue = useCallback10(
5915
+ (mockupId) => {
5916
+ if (compositeBusyRef.current.has(mockupId)) return;
5917
+ const job = compositeQueueRef.current.get(mockupId);
5918
+ if (!job) return;
5919
+ compositeQueueRef.current.delete(mockupId);
5920
+ compositeBusyRef.current.add(mockupId);
5921
+ void runComposite(job).finally(() => {
5922
+ compositeBusyRef.current.delete(mockupId);
5923
+ pumpCompositeQueue(mockupId);
5924
+ });
5925
+ },
5926
+ [runComposite]
5927
+ );
5928
+ useEffect17(() => {
5929
+ if (rawMockupResults.length === 0) {
5930
+ if (mockupResultsRef.current.length > 0) {
5931
+ mockupResultsRef.current = [];
5932
+ mockupSubscribersRef.current.forEach((cb) => cb([]));
5933
+ }
5934
+ return;
5729
5935
  }
5730
- mockupSubscribersRef.current.forEach((cb) => cb(rawMockupResults));
5936
+ console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
5937
+ const displayable = [];
5731
5938
  rawMockupResults.forEach((result) => {
5732
- const subscribers = mockupSubscribersByIdRef.current.get(result.mockupId);
5733
- if (subscribers && subscribers.size > 0) {
5734
- console.log(`[RTP] Notifying ${subscribers.size} subscriber(s) for ${result.mockupId}`);
5735
- subscribers.forEach((cb) => cb(result));
5736
- } else {
5737
- console.log(`[RTP] No subscribers for ${result.mockupId} \u2014 result received but nobody listening`);
5939
+ if (!result.imageUrl) {
5940
+ console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
5941
+ return;
5942
+ }
5943
+ const firstSight = processedFrameRef.current.get(result.mockupId) !== result.imageUrl;
5944
+ const sessionLive = configSentRef.current;
5945
+ if (isFullRect(result.cropRect)) {
5946
+ if (firstSight && sessionLive) {
5947
+ processedFrameRef.current.set(result.mockupId, result.imageUrl);
5948
+ const imgPromise = loadImageElement(result.imageUrl);
5949
+ imgPromise.catch(() => {
5950
+ });
5951
+ backgroundsRef.current.set(result.mockupId, { url: result.imageUrl, imgPromise });
5952
+ }
5953
+ displayable.push(result);
5954
+ } else if (firstSight && sessionLive) {
5955
+ processedFrameRef.current.set(result.mockupId, result.imageUrl);
5956
+ compositeQueueRef.current.set(result.mockupId, result);
5957
+ pumpCompositeQueue(result.mockupId);
5958
+ } else if (pendingMockupIdsRef.current.has(result.mockupId)) {
5959
+ const cached = mockupUrlCacheRef.current.get(result.mockupId);
5960
+ if (cached) displayable.push({ ...result, imageUrl: cached.url });
5738
5961
  }
5739
5962
  });
5740
- if (hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
5741
- hasBlobInFlightRef.current = false;
5742
- pipelineSettledSubscribersRef.current.forEach((cb) => cb());
5743
- }
5744
- }, [rawMockupResults]);
5963
+ deliverResults(displayable);
5964
+ maybeFlipCrop();
5965
+ }, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
5745
5966
  const sendCanvasBlob = useCallback10(
5746
5967
  (placement, blob, mockupCount, baseThrottleMs) => {
5747
5968
  blobQueuedGenRef.current = pipelineGenRef.current;
@@ -5802,9 +6023,11 @@ function RealtimeProvider({
5802
6023
  lastSentBgRef.current = void 0;
5803
6024
  lastSentSpreadCropsRef.current = "null";
5804
6025
  lastSentColorsRef.current = {};
6026
+ invalidateBackgrounds();
6027
+ lastConfigBaseRef.current = null;
5805
6028
  };
5806
6029
  }
5807
- }, [isEnabled, connect, disconnect, wsUrl]);
6030
+ }, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
5808
6031
  useEffect17(() => {
5809
6032
  if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
5810
6033
  isPendingMockupsRef.current = false;
@@ -5828,6 +6051,9 @@ function RealtimeProvider({
5828
6051
  if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
5829
6052
  return;
5830
6053
  }
6054
+ if (variantChanged || bgChanged) {
6055
+ invalidateBackgrounds();
6056
+ }
5831
6057
  const mockupIds = [];
5832
6058
  if (product.mockups && Array.isArray(product.mockups)) {
5833
6059
  product.mockups.forEach((mockup) => {
@@ -5862,7 +6088,10 @@ function RealtimeProvider({
5862
6088
  pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
5863
6089
  });
5864
6090
  }
5865
- sendConfigRaw(configPayload);
6091
+ lastConfigBaseRef.current = configPayload;
6092
+ sendConfigRaw(
6093
+ croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6094
+ );
5866
6095
  configSentRef.current = true;
5867
6096
  lastSentVariantIdRef.current = variantId;
5868
6097
  lastSentBgRef.current = mockupBg;
@@ -5884,6 +6113,7 @@ function RealtimeProvider({
5884
6113
  productContext?.selection,
5885
6114
  sendConfigRaw,
5886
6115
  sendColorBlobTracked,
6116
+ invalidateBackgrounds,
5887
6117
  mockupWidth,
5888
6118
  mockupBg,
5889
6119
  spreadCropsVersion
@@ -5913,6 +6143,15 @@ function RealtimeProvider({
5913
6143
  productContext?.selection,
5914
6144
  sendColorBlobTracked
5915
6145
  ]);
6146
+ const updateMockupIdsGuarded = useCallback10(
6147
+ (mockupIds) => {
6148
+ if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6149
+ unflipCrop();
6150
+ }
6151
+ return updateMockupIdsRaw(mockupIds);
6152
+ },
6153
+ [updateMockupIdsRaw, unflipCrop]
6154
+ );
5916
6155
  const updatePlacementSettingsWrapped = useCallback10(
5917
6156
  (settings) => {
5918
6157
  placementSettingsOverridesRef.current = settings;
@@ -6112,28 +6351,6 @@ function RealtimeProvider({
6112
6351
  const isMockupLoading = useCallback10((mockupId) => {
6113
6352
  return pendingMockupIdsRef.current.has(mockupId);
6114
6353
  }, []);
6115
- const registerBlobUrl = useCallback10((blobOrUrl, placementName) => {
6116
- let url;
6117
- if (blobOrUrl instanceof Blob) {
6118
- if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
6119
- const oldest = registeredBlobUrlsRef.current.values().next().value;
6120
- if (oldest) {
6121
- URL.revokeObjectURL(oldest);
6122
- registeredBlobUrlsRef.current.delete(oldest);
6123
- mockupUrlCacheRef.current.forEach((value, key) => {
6124
- if (value.url === oldest) {
6125
- mockupUrlCacheRef.current.delete(key);
6126
- }
6127
- });
6128
- }
6129
- }
6130
- url = URL.createObjectURL(blobOrUrl);
6131
- registeredBlobUrlsRef.current.add(url);
6132
- } else {
6133
- url = blobOrUrl;
6134
- }
6135
- return url;
6136
- }, []);
6137
6354
  const subscribeMockupResultsRef = useRef12(subscribeMockupResults);
6138
6355
  const subscribeMockupResultByIdRef = useRef12(subscribeMockupResultById);
6139
6356
  const subscribePendingBlobRef = useRef12(subscribePendingBlob);
@@ -6214,7 +6431,7 @@ function RealtimeProvider({
6214
6431
  disableRealtime,
6215
6432
  sendCanvasBlob,
6216
6433
  sendCanvasState,
6217
- updateMockupIds: updateMockupIdsRaw,
6434
+ updateMockupIds: updateMockupIdsGuarded,
6218
6435
  updatePlacementSettings: updatePlacementSettingsWrapped,
6219
6436
  setSpreadCrops,
6220
6437
  getMockupResultsImmediate: () => mockupResultsRef.current,
@@ -6251,7 +6468,7 @@ function RealtimeProvider({
6251
6468
  disableRealtime,
6252
6469
  sendCanvasBlob,
6253
6470
  sendCanvasState,
6254
- updateMockupIdsRaw,
6471
+ updateMockupIdsGuarded,
6255
6472
  updatePlacementSettingsWrapped,
6256
6473
  setSpreadCrops,
6257
6474
  stableSubscribeMockupResults,
@@ -9768,7 +9985,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
9768
9985
  // Quick crossfade
9769
9986
  shimmerDelay = 200,
9770
9987
  shimmerFadeDuration = 500,
9771
- darkShimmerFadeInDuration = 300
9988
+ darkShimmerFadeInDuration = 300,
9989
+ emptyText = "No artwork",
9990
+ errorText = "Error loading",
9991
+ unavailableText = "Preview unavailable"
9772
9992
  }) {
9773
9993
  const context = useProductOptional();
9774
9994
  const designContext = useDesignOptional();
@@ -10213,7 +10433,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10213
10433
  )
10214
10434
  }
10215
10435
  ),
10216
- /* @__PURE__ */ jsx61("p", { className: "text-xs", children: "No artwork" })
10436
+ /* @__PURE__ */ jsx61("p", { className: "text-xs", children: emptyText })
10217
10437
  ] })
10218
10438
  }
10219
10439
  );
@@ -10224,7 +10444,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10224
10444
  {
10225
10445
  className: `bg-red-50 flex items-center justify-center ${className || ""}`,
10226
10446
  style,
10227
- children: /* @__PURE__ */ jsx61("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ jsx61("p", { className: "text-xs", children: "Error loading" }) })
10447
+ children: /* @__PURE__ */ jsx61("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ jsx61("p", { className: "text-xs", children: errorText }) })
10228
10448
  }
10229
10449
  );
10230
10450
  }
@@ -10243,7 +10463,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10243
10463
  crossOrigin: "anonymous",
10244
10464
  className: "absolute inset-0 w-full h-full object-cover",
10245
10465
  draggable,
10246
- src: retriedUrlRef.current === renderedUrl ? `${renderedUrl}${renderedUrl.includes("?") ? "&" : "?"}_cb=${retryNonce}` : renderedUrl,
10466
+ src: renderedUrl,
10247
10467
  loading: "eager",
10248
10468
  fetchPriority: "high",
10249
10469
  onClick,
@@ -10259,7 +10479,8 @@ var HeroProductImage = memo3(function HeroProductImage2({
10259
10479
  onError: () => {
10260
10480
  if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
10261
10481
  retriedUrlRef.current = renderedUrl;
10262
- setRetryNonce((n) => n + 1);
10482
+ void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
10483
+ }).then(() => setRetryNonce((n) => n + 1));
10263
10484
  return;
10264
10485
  }
10265
10486
  setImgFailed(true);
@@ -10273,9 +10494,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
10273
10494
  );
10274
10495
  }
10275
10496
  }
10276
- }
10497
+ },
10498
+ retryNonce
10277
10499
  ),
10278
- imgFailed && !prevUrl && /* @__PURE__ */ jsx61("div", { className: "absolute inset-0 flex items-center justify-center bg-muted", children: /* @__PURE__ */ jsx61("p", { className: "text-muted-foreground text-xs", children: "Preview unavailable" }) }),
10500
+ imgFailed && !prevUrl && /* @__PURE__ */ jsx61("div", { className: "absolute inset-0 flex items-center justify-center bg-muted", children: /* @__PURE__ */ jsx61("p", { className: "text-muted-foreground text-xs", children: unavailableText }) }),
10279
10501
  realtimeContext?.renderError && /* @__PURE__ */ jsx61(
10280
10502
  "div",
10281
10503
  {
@@ -11784,7 +12006,10 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
11784
12006
  onZoomChange,
11785
12007
  stickyHeroContext,
11786
12008
  mockupWidth,
11787
- onMockupUrlGenerated
12009
+ onMockupUrlGenerated,
12010
+ renderImageAction,
12011
+ errorText,
12012
+ unavailableText
11788
12013
  }) {
11789
12014
  const [activeIndex, setActiveIndex] = useState40(currentIndex);
11790
12015
  const [touchStart, setTouchStart] = useState40(
@@ -12318,34 +12543,52 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
12318
12543
  children: images.map((image, index) => {
12319
12544
  const DECODE_WINDOW = 1;
12320
12545
  const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
12321
- return /* @__PURE__ */ jsx74(
12546
+ return /* @__PURE__ */ jsxs50(
12322
12547
  "div",
12323
12548
  {
12324
12549
  className: "w-full h-full flex-shrink-0 relative select-none",
12325
- children: !isWithinDecodeWindow ? /* @__PURE__ */ jsx74("div", { className: "w-full h-full bg-muted" }) : image.isPlaceholder ? (
12326
- // Rainbow prism loader (same as the /create route) instead of
12327
- // a flat pulse; fills this relative slide via inset:0.
12328
- /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" })
12329
- ) : image.isRealMockup && currentArtwork ? /* @__PURE__ */ jsx74(
12330
- HeroProductImage,
12331
- {
12332
- productId,
12333
- artwork: currentArtwork,
12334
- placement: image.placement,
12335
- mockupId: image.mockupId,
12336
- width: mockupWidth,
12337
- realtimeUrl: image.mockupId ? realtimeMockupUrls[image.mockupId] ?? generatedUrlsRef.current.get(image.mockupId) ?? void 0 : void 0,
12338
- onUrlGenerated: image.mockupId ? (url) => handleUrlGenerated(image.mockupId, url) : void 0,
12339
- className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12340
- }
12341
- ) : image.isRealMockup && !currentArtwork ? /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" }) : /* @__PURE__ */ jsx74(
12342
- StaticCarouselImage,
12343
- {
12344
- src: image.src,
12345
- alt: image.label ?? "",
12346
- className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12347
- }
12348
- )
12550
+ children: [
12551
+ !isWithinDecodeWindow ? /* @__PURE__ */ jsx74("div", { className: "w-full h-full bg-muted" }) : image.isPlaceholder ? (
12552
+ // Rainbow prism loader (same as the /create route) instead of
12553
+ // a flat pulse; fills this relative slide via inset:0.
12554
+ /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" })
12555
+ ) : image.isRealMockup && currentArtwork ? /* @__PURE__ */ jsx74(
12556
+ HeroProductImage,
12557
+ {
12558
+ productId,
12559
+ artwork: currentArtwork,
12560
+ placement: image.placement,
12561
+ mockupId: image.mockupId,
12562
+ width: mockupWidth,
12563
+ realtimeUrl: image.mockupId ? realtimeMockupUrls[image.mockupId] ?? generatedUrlsRef.current.get(image.mockupId) ?? void 0 : void 0,
12564
+ onUrlGenerated: image.mockupId ? (url) => handleUrlGenerated(image.mockupId, url) : void 0,
12565
+ errorText,
12566
+ unavailableText,
12567
+ className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12568
+ }
12569
+ ) : image.isRealMockup && !currentArtwork ? /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" }) : /* @__PURE__ */ jsx74(
12570
+ StaticCarouselImage,
12571
+ {
12572
+ src: image.src,
12573
+ alt: image.label ?? "",
12574
+ className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12575
+ }
12576
+ ),
12577
+ isWithinDecodeWindow && (() => {
12578
+ const action = renderImageAction?.(image, index);
12579
+ return action ? /* @__PURE__ */ jsx74(
12580
+ "div",
12581
+ {
12582
+ className: "absolute top-3 right-3 z-10",
12583
+ onClick: (e) => e.stopPropagation(),
12584
+ onTouchStart: (e) => e.stopPropagation(),
12585
+ onTouchEnd: (e) => e.stopPropagation(),
12586
+ onMouseDown: (e) => e.stopPropagation(),
12587
+ children: action
12588
+ }
12589
+ ) : null;
12590
+ })()
12591
+ ]
12349
12592
  },
12350
12593
  index
12351
12594
  );