@snowcone-app/ui 0.4.9 → 0.4.10

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,214 @@ 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
+ backgroundsRef.current.set(result.mockupId, { url: result.imageUrl });
5949
+ }
5950
+ displayable.push(result);
5951
+ } else if (firstSight && sessionLive) {
5952
+ processedFrameRef.current.set(result.mockupId, result.imageUrl);
5953
+ compositeQueueRef.current.set(result.mockupId, result);
5954
+ pumpCompositeQueue(result.mockupId);
5955
+ } else if (pendingMockupIdsRef.current.has(result.mockupId)) {
5956
+ const cached = mockupUrlCacheRef.current.get(result.mockupId);
5957
+ if (cached) displayable.push({ ...result, imageUrl: cached.url });
5738
5958
  }
5739
5959
  });
5740
- if (hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
5741
- hasBlobInFlightRef.current = false;
5742
- pipelineSettledSubscribersRef.current.forEach((cb) => cb());
5743
- }
5744
- }, [rawMockupResults]);
5960
+ deliverResults(displayable);
5961
+ maybeFlipCrop();
5962
+ }, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
5745
5963
  const sendCanvasBlob = useCallback10(
5746
5964
  (placement, blob, mockupCount, baseThrottleMs) => {
5747
5965
  blobQueuedGenRef.current = pipelineGenRef.current;
@@ -5802,9 +6020,11 @@ function RealtimeProvider({
5802
6020
  lastSentBgRef.current = void 0;
5803
6021
  lastSentSpreadCropsRef.current = "null";
5804
6022
  lastSentColorsRef.current = {};
6023
+ invalidateBackgrounds();
6024
+ lastConfigBaseRef.current = null;
5805
6025
  };
5806
6026
  }
5807
- }, [isEnabled, connect, disconnect, wsUrl]);
6027
+ }, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
5808
6028
  useEffect17(() => {
5809
6029
  if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
5810
6030
  isPendingMockupsRef.current = false;
@@ -5828,6 +6048,9 @@ function RealtimeProvider({
5828
6048
  if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
5829
6049
  return;
5830
6050
  }
6051
+ if (variantChanged || bgChanged) {
6052
+ invalidateBackgrounds();
6053
+ }
5831
6054
  const mockupIds = [];
5832
6055
  if (product.mockups && Array.isArray(product.mockups)) {
5833
6056
  product.mockups.forEach((mockup) => {
@@ -5862,7 +6085,10 @@ function RealtimeProvider({
5862
6085
  pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
5863
6086
  });
5864
6087
  }
5865
- sendConfigRaw(configPayload);
6088
+ lastConfigBaseRef.current = configPayload;
6089
+ sendConfigRaw(
6090
+ croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6091
+ );
5866
6092
  configSentRef.current = true;
5867
6093
  lastSentVariantIdRef.current = variantId;
5868
6094
  lastSentBgRef.current = mockupBg;
@@ -5884,6 +6110,7 @@ function RealtimeProvider({
5884
6110
  productContext?.selection,
5885
6111
  sendConfigRaw,
5886
6112
  sendColorBlobTracked,
6113
+ invalidateBackgrounds,
5887
6114
  mockupWidth,
5888
6115
  mockupBg,
5889
6116
  spreadCropsVersion
@@ -5913,6 +6140,15 @@ function RealtimeProvider({
5913
6140
  productContext?.selection,
5914
6141
  sendColorBlobTracked
5915
6142
  ]);
6143
+ const updateMockupIdsGuarded = useCallback10(
6144
+ (mockupIds) => {
6145
+ if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6146
+ unflipCrop();
6147
+ }
6148
+ return updateMockupIdsRaw(mockupIds);
6149
+ },
6150
+ [updateMockupIdsRaw, unflipCrop]
6151
+ );
5916
6152
  const updatePlacementSettingsWrapped = useCallback10(
5917
6153
  (settings) => {
5918
6154
  placementSettingsOverridesRef.current = settings;
@@ -6112,28 +6348,6 @@ function RealtimeProvider({
6112
6348
  const isMockupLoading = useCallback10((mockupId) => {
6113
6349
  return pendingMockupIdsRef.current.has(mockupId);
6114
6350
  }, []);
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
6351
  const subscribeMockupResultsRef = useRef12(subscribeMockupResults);
6138
6352
  const subscribeMockupResultByIdRef = useRef12(subscribeMockupResultById);
6139
6353
  const subscribePendingBlobRef = useRef12(subscribePendingBlob);
@@ -6214,7 +6428,7 @@ function RealtimeProvider({
6214
6428
  disableRealtime,
6215
6429
  sendCanvasBlob,
6216
6430
  sendCanvasState,
6217
- updateMockupIds: updateMockupIdsRaw,
6431
+ updateMockupIds: updateMockupIdsGuarded,
6218
6432
  updatePlacementSettings: updatePlacementSettingsWrapped,
6219
6433
  setSpreadCrops,
6220
6434
  getMockupResultsImmediate: () => mockupResultsRef.current,
@@ -6251,7 +6465,7 @@ function RealtimeProvider({
6251
6465
  disableRealtime,
6252
6466
  sendCanvasBlob,
6253
6467
  sendCanvasState,
6254
- updateMockupIdsRaw,
6468
+ updateMockupIdsGuarded,
6255
6469
  updatePlacementSettingsWrapped,
6256
6470
  setSpreadCrops,
6257
6471
  stableSubscribeMockupResults,
@@ -9768,7 +9982,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
9768
9982
  // Quick crossfade
9769
9983
  shimmerDelay = 200,
9770
9984
  shimmerFadeDuration = 500,
9771
- darkShimmerFadeInDuration = 300
9985
+ darkShimmerFadeInDuration = 300,
9986
+ emptyText = "No artwork",
9987
+ errorText = "Error loading",
9988
+ unavailableText = "Preview unavailable"
9772
9989
  }) {
9773
9990
  const context = useProductOptional();
9774
9991
  const designContext = useDesignOptional();
@@ -10213,7 +10430,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10213
10430
  )
10214
10431
  }
10215
10432
  ),
10216
- /* @__PURE__ */ jsx61("p", { className: "text-xs", children: "No artwork" })
10433
+ /* @__PURE__ */ jsx61("p", { className: "text-xs", children: emptyText })
10217
10434
  ] })
10218
10435
  }
10219
10436
  );
@@ -10224,7 +10441,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10224
10441
  {
10225
10442
  className: `bg-red-50 flex items-center justify-center ${className || ""}`,
10226
10443
  style,
10227
- children: /* @__PURE__ */ jsx61("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ jsx61("p", { className: "text-xs", children: "Error loading" }) })
10444
+ children: /* @__PURE__ */ jsx61("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ jsx61("p", { className: "text-xs", children: errorText }) })
10228
10445
  }
10229
10446
  );
10230
10447
  }
@@ -10243,7 +10460,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
10243
10460
  crossOrigin: "anonymous",
10244
10461
  className: "absolute inset-0 w-full h-full object-cover",
10245
10462
  draggable,
10246
- src: retriedUrlRef.current === renderedUrl ? `${renderedUrl}${renderedUrl.includes("?") ? "&" : "?"}_cb=${retryNonce}` : renderedUrl,
10463
+ src: renderedUrl,
10247
10464
  loading: "eager",
10248
10465
  fetchPriority: "high",
10249
10466
  onClick,
@@ -10259,7 +10476,8 @@ var HeroProductImage = memo3(function HeroProductImage2({
10259
10476
  onError: () => {
10260
10477
  if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
10261
10478
  retriedUrlRef.current = renderedUrl;
10262
- setRetryNonce((n) => n + 1);
10479
+ void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
10480
+ }).then(() => setRetryNonce((n) => n + 1));
10263
10481
  return;
10264
10482
  }
10265
10483
  setImgFailed(true);
@@ -10273,9 +10491,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
10273
10491
  );
10274
10492
  }
10275
10493
  }
10276
- }
10494
+ },
10495
+ retryNonce
10277
10496
  ),
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" }) }),
10497
+ 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
10498
  realtimeContext?.renderError && /* @__PURE__ */ jsx61(
10280
10499
  "div",
10281
10500
  {
@@ -11784,7 +12003,10 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
11784
12003
  onZoomChange,
11785
12004
  stickyHeroContext,
11786
12005
  mockupWidth,
11787
- onMockupUrlGenerated
12006
+ onMockupUrlGenerated,
12007
+ renderImageAction,
12008
+ errorText,
12009
+ unavailableText
11788
12010
  }) {
11789
12011
  const [activeIndex, setActiveIndex] = useState40(currentIndex);
11790
12012
  const [touchStart, setTouchStart] = useState40(
@@ -12318,34 +12540,52 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
12318
12540
  children: images.map((image, index) => {
12319
12541
  const DECODE_WINDOW = 1;
12320
12542
  const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
12321
- return /* @__PURE__ */ jsx74(
12543
+ return /* @__PURE__ */ jsxs50(
12322
12544
  "div",
12323
12545
  {
12324
12546
  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
- )
12547
+ children: [
12548
+ !isWithinDecodeWindow ? /* @__PURE__ */ jsx74("div", { className: "w-full h-full bg-muted" }) : image.isPlaceholder ? (
12549
+ // Rainbow prism loader (same as the /create route) instead of
12550
+ // a flat pulse; fills this relative slide via inset:0.
12551
+ /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" })
12552
+ ) : image.isRealMockup && currentArtwork ? /* @__PURE__ */ jsx74(
12553
+ HeroProductImage,
12554
+ {
12555
+ productId,
12556
+ artwork: currentArtwork,
12557
+ placement: image.placement,
12558
+ mockupId: image.mockupId,
12559
+ width: mockupWidth,
12560
+ realtimeUrl: image.mockupId ? realtimeMockupUrls[image.mockupId] ?? generatedUrlsRef.current.get(image.mockupId) ?? void 0 : void 0,
12561
+ onUrlGenerated: image.mockupId ? (url) => handleUrlGenerated(image.mockupId, url) : void 0,
12562
+ errorText,
12563
+ unavailableText,
12564
+ className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12565
+ }
12566
+ ) : image.isRealMockup && !currentArtwork ? /* @__PURE__ */ jsx74(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" }) : /* @__PURE__ */ jsx74(
12567
+ StaticCarouselImage,
12568
+ {
12569
+ src: image.src,
12570
+ alt: image.label ?? "",
12571
+ className: "w-full h-full object-cover cursor-pointer pointer-events-none"
12572
+ }
12573
+ ),
12574
+ isWithinDecodeWindow && (() => {
12575
+ const action = renderImageAction?.(image, index);
12576
+ return action ? /* @__PURE__ */ jsx74(
12577
+ "div",
12578
+ {
12579
+ className: "absolute top-3 right-3 z-10",
12580
+ onClick: (e) => e.stopPropagation(),
12581
+ onTouchStart: (e) => e.stopPropagation(),
12582
+ onTouchEnd: (e) => e.stopPropagation(),
12583
+ onMouseDown: (e) => e.stopPropagation(),
12584
+ children: action
12585
+ }
12586
+ ) : null;
12587
+ })()
12588
+ ]
12349
12589
  },
12350
12590
  index
12351
12591
  );