@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/CHANGELOG.md +13 -0
- package/dist/index.cjs +338 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +338 -95
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composed/HeroProductImage.tsx +39 -25
- package/src/composed/carousels/MobileProductCarousel.tsx +38 -0
- package/src/patterns/RealtimeProvider.tsx +352 -82
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +261 -0
- package/src/patterns/__tests__/croppedFrameComposite.test.ts +56 -0
- package/src/patterns/croppedFrameComposite.ts +122 -0
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
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
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
|
-
|
|
5782
|
+
url = URL.createObjectURL(blobOrUrl);
|
|
5783
|
+
registeredBlobUrlsRef.current.add(url);
|
|
5784
|
+
} else {
|
|
5785
|
+
url = blobOrUrl;
|
|
5700
5786
|
}
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
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:
|
|
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
|
-
|
|
5857
|
+
pendingPlacementsRef.current.clear();
|
|
5719
5858
|
}
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
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
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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__ */
|
|
12546
|
+
return /* @__PURE__ */ jsxs50(
|
|
12322
12547
|
"div",
|
|
12323
12548
|
{
|
|
12324
12549
|
className: "w-full h-full flex-shrink-0 relative select-none",
|
|
12325
|
-
children:
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
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
|
);
|