@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/CHANGELOG.md +7 -0
- package/dist/index.cjs +335 -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 +335 -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 +341 -82
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +240 -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,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
|
-
|
|
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
|
+
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
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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__ */
|
|
12543
|
+
return /* @__PURE__ */ jsxs50(
|
|
12322
12544
|
"div",
|
|
12323
12545
|
{
|
|
12324
12546
|
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
|
-
|
|
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
|
);
|