@snowcone-app/ui 0.4.8 → 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 +16 -0
- package/dist/index.cjs +360 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +360 -97
- 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 +414 -83
- 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();
|
|
@@ -5611,6 +5673,9 @@ function RealtimeProvider({
|
|
|
5611
5673
|
const placementSettingsOverridesRef = useRef12({});
|
|
5612
5674
|
const lastSentVariantIdRef = useRef12(null);
|
|
5613
5675
|
const lastSentBgRef = useRef12(void 0);
|
|
5676
|
+
const spreadCropsRef = useRef12(void 0);
|
|
5677
|
+
const [spreadCropsVersion, setSpreadCropsVersion] = useState17(0);
|
|
5678
|
+
const lastSentSpreadCropsRef = useRef12("null");
|
|
5614
5679
|
const lastSentColorsRef = useRef12({});
|
|
5615
5680
|
const realtimeMockup = useRealtimeMockup2({
|
|
5616
5681
|
wsUrl,
|
|
@@ -5687,58 +5752,214 @@ function RealtimeProvider({
|
|
|
5687
5752
|
const registeredBlobUrlsRef = useRef12(/* @__PURE__ */ new Set());
|
|
5688
5753
|
const pendingPlacementsRef = useRef12(/* @__PURE__ */ new Set());
|
|
5689
5754
|
const pendingMockupIdsRef = useRef12(/* @__PURE__ */ new Set());
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
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
|
+
}
|
|
5695
5781
|
}
|
|
5696
|
-
|
|
5782
|
+
url = URL.createObjectURL(blobOrUrl);
|
|
5783
|
+
registeredBlobUrlsRef.current.add(url);
|
|
5784
|
+
} else {
|
|
5785
|
+
url = blobOrUrl;
|
|
5697
5786
|
}
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
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) => {
|
|
5703
5842
|
const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
|
|
5704
5843
|
mockupUrlCacheRef.current.set(result.mockupId, {
|
|
5705
5844
|
url: result.imageUrl,
|
|
5706
|
-
isBlob:
|
|
5707
|
-
// Server URLs are not blob URLs
|
|
5845
|
+
isBlob: result.imageUrl.startsWith("blob:"),
|
|
5708
5846
|
timestamp: now
|
|
5709
5847
|
});
|
|
5710
5848
|
pendingMockupIdsRef.current.delete(result.mockupId);
|
|
5711
5849
|
if (wasPending) {
|
|
5712
5850
|
console.log(`[RTP] Cleared pending for ${result.mockupId} \u2014 url cached`);
|
|
5713
5851
|
}
|
|
5852
|
+
if (result.placement) clearedPlacements.add(result.placement);
|
|
5853
|
+
});
|
|
5854
|
+
if (clearedPlacements.size > 0) {
|
|
5855
|
+
clearedPlacements.forEach((p) => pendingPlacementsRef.current.delete(p));
|
|
5714
5856
|
} else {
|
|
5715
|
-
|
|
5857
|
+
pendingPlacementsRef.current.clear();
|
|
5716
5858
|
}
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
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;
|
|
5726
5935
|
}
|
|
5727
|
-
|
|
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 = [];
|
|
5728
5938
|
rawMockupResults.forEach((result) => {
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
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 });
|
|
5735
5958
|
}
|
|
5736
5959
|
});
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
}
|
|
5741
|
-
}, [rawMockupResults]);
|
|
5960
|
+
deliverResults(displayable);
|
|
5961
|
+
maybeFlipCrop();
|
|
5962
|
+
}, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
|
|
5742
5963
|
const sendCanvasBlob = useCallback10(
|
|
5743
5964
|
(placement, blob, mockupCount, baseThrottleMs) => {
|
|
5744
5965
|
blobQueuedGenRef.current = pipelineGenRef.current;
|
|
@@ -5797,10 +6018,13 @@ function RealtimeProvider({
|
|
|
5797
6018
|
configSentRef.current = false;
|
|
5798
6019
|
lastSentVariantIdRef.current = null;
|
|
5799
6020
|
lastSentBgRef.current = void 0;
|
|
6021
|
+
lastSentSpreadCropsRef.current = "null";
|
|
5800
6022
|
lastSentColorsRef.current = {};
|
|
6023
|
+
invalidateBackgrounds();
|
|
6024
|
+
lastConfigBaseRef.current = null;
|
|
5801
6025
|
};
|
|
5802
6026
|
}
|
|
5803
|
-
}, [isEnabled, connect, disconnect, wsUrl]);
|
|
6027
|
+
}, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
|
|
5804
6028
|
useEffect17(() => {
|
|
5805
6029
|
if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
|
|
5806
6030
|
isPendingMockupsRef.current = false;
|
|
@@ -5819,9 +6043,14 @@ function RealtimeProvider({
|
|
|
5819
6043
|
const isFirstSend = !configSentRef.current;
|
|
5820
6044
|
const variantChanged = lastSentVariantIdRef.current !== null && lastSentVariantIdRef.current !== variantId;
|
|
5821
6045
|
const bgChanged = configSentRef.current && lastSentBgRef.current !== mockupBg;
|
|
5822
|
-
|
|
6046
|
+
const spreadCropsJson = JSON.stringify(spreadCropsRef.current ?? null);
|
|
6047
|
+
const spreadCropsChanged = configSentRef.current && lastSentSpreadCropsRef.current !== spreadCropsJson;
|
|
6048
|
+
if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
|
|
5823
6049
|
return;
|
|
5824
6050
|
}
|
|
6051
|
+
if (variantChanged || bgChanged) {
|
|
6052
|
+
invalidateBackgrounds();
|
|
6053
|
+
}
|
|
5825
6054
|
const mockupIds = [];
|
|
5826
6055
|
if (product.mockups && Array.isArray(product.mockups)) {
|
|
5827
6056
|
product.mockups.forEach((mockup) => {
|
|
@@ -5837,6 +6066,7 @@ function RealtimeProvider({
|
|
|
5837
6066
|
shop,
|
|
5838
6067
|
width: mockupWidth,
|
|
5839
6068
|
...mockupBg ? { bg: mockupBg } : {},
|
|
6069
|
+
...spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {},
|
|
5840
6070
|
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
5841
6071
|
} : {
|
|
5842
6072
|
productId,
|
|
@@ -5846,6 +6076,7 @@ function RealtimeProvider({
|
|
|
5846
6076
|
seal: "bypass-sig-for-k6-load-test",
|
|
5847
6077
|
width: mockupWidth,
|
|
5848
6078
|
...mockupBg ? { bg: mockupBg } : {},
|
|
6079
|
+
...spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {},
|
|
5849
6080
|
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
5850
6081
|
};
|
|
5851
6082
|
if (variantChanged) {
|
|
@@ -5854,10 +6085,14 @@ function RealtimeProvider({
|
|
|
5854
6085
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
|
|
5855
6086
|
});
|
|
5856
6087
|
}
|
|
5857
|
-
|
|
6088
|
+
lastConfigBaseRef.current = configPayload;
|
|
6089
|
+
sendConfigRaw(
|
|
6090
|
+
croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
|
|
6091
|
+
);
|
|
5858
6092
|
configSentRef.current = true;
|
|
5859
6093
|
lastSentVariantIdRef.current = variantId;
|
|
5860
6094
|
lastSentBgRef.current = mockupBg;
|
|
6095
|
+
lastSentSpreadCropsRef.current = spreadCropsJson;
|
|
5861
6096
|
setTimeout(() => {
|
|
5862
6097
|
const colorPlacements = placements.filter((p) => p.type === "color");
|
|
5863
6098
|
colorPlacements.forEach((p) => {
|
|
@@ -5875,8 +6110,10 @@ function RealtimeProvider({
|
|
|
5875
6110
|
productContext?.selection,
|
|
5876
6111
|
sendConfigRaw,
|
|
5877
6112
|
sendColorBlobTracked,
|
|
6113
|
+
invalidateBackgrounds,
|
|
5878
6114
|
mockupWidth,
|
|
5879
|
-
mockupBg
|
|
6115
|
+
mockupBg,
|
|
6116
|
+
spreadCropsVersion
|
|
5880
6117
|
]);
|
|
5881
6118
|
useEffect17(() => {
|
|
5882
6119
|
if (!isEnabled || !isConnected || !isConfigured || !productContext?.product)
|
|
@@ -5903,6 +6140,15 @@ function RealtimeProvider({
|
|
|
5903
6140
|
productContext?.selection,
|
|
5904
6141
|
sendColorBlobTracked
|
|
5905
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
|
+
);
|
|
5906
6152
|
const updatePlacementSettingsWrapped = useCallback10(
|
|
5907
6153
|
(settings) => {
|
|
5908
6154
|
placementSettingsOverridesRef.current = settings;
|
|
@@ -5910,6 +6156,17 @@ function RealtimeProvider({
|
|
|
5910
6156
|
},
|
|
5911
6157
|
[updatePlacementSettingsRaw]
|
|
5912
6158
|
);
|
|
6159
|
+
const setSpreadCrops = useCallback10(
|
|
6160
|
+
(crops) => {
|
|
6161
|
+
const next = crops && Object.keys(crops).length > 0 ? crops : void 0;
|
|
6162
|
+
if (JSON.stringify(next ?? null) === JSON.stringify(spreadCropsRef.current ?? null)) {
|
|
6163
|
+
return;
|
|
6164
|
+
}
|
|
6165
|
+
spreadCropsRef.current = next;
|
|
6166
|
+
setSpreadCropsVersion((v) => v + 1);
|
|
6167
|
+
},
|
|
6168
|
+
[]
|
|
6169
|
+
);
|
|
5913
6170
|
const enableRealtime = useCallback10(() => {
|
|
5914
6171
|
setIsEnabled(true);
|
|
5915
6172
|
}, []);
|
|
@@ -6091,28 +6348,6 @@ function RealtimeProvider({
|
|
|
6091
6348
|
const isMockupLoading = useCallback10((mockupId) => {
|
|
6092
6349
|
return pendingMockupIdsRef.current.has(mockupId);
|
|
6093
6350
|
}, []);
|
|
6094
|
-
const registerBlobUrl = useCallback10((blobOrUrl, placementName) => {
|
|
6095
|
-
let url;
|
|
6096
|
-
if (blobOrUrl instanceof Blob) {
|
|
6097
|
-
if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
|
|
6098
|
-
const oldest = registeredBlobUrlsRef.current.values().next().value;
|
|
6099
|
-
if (oldest) {
|
|
6100
|
-
URL.revokeObjectURL(oldest);
|
|
6101
|
-
registeredBlobUrlsRef.current.delete(oldest);
|
|
6102
|
-
mockupUrlCacheRef.current.forEach((value, key) => {
|
|
6103
|
-
if (value.url === oldest) {
|
|
6104
|
-
mockupUrlCacheRef.current.delete(key);
|
|
6105
|
-
}
|
|
6106
|
-
});
|
|
6107
|
-
}
|
|
6108
|
-
}
|
|
6109
|
-
url = URL.createObjectURL(blobOrUrl);
|
|
6110
|
-
registeredBlobUrlsRef.current.add(url);
|
|
6111
|
-
} else {
|
|
6112
|
-
url = blobOrUrl;
|
|
6113
|
-
}
|
|
6114
|
-
return url;
|
|
6115
|
-
}, []);
|
|
6116
6351
|
const subscribeMockupResultsRef = useRef12(subscribeMockupResults);
|
|
6117
6352
|
const subscribeMockupResultByIdRef = useRef12(subscribeMockupResultById);
|
|
6118
6353
|
const subscribePendingBlobRef = useRef12(subscribePendingBlob);
|
|
@@ -6193,8 +6428,9 @@ function RealtimeProvider({
|
|
|
6193
6428
|
disableRealtime,
|
|
6194
6429
|
sendCanvasBlob,
|
|
6195
6430
|
sendCanvasState,
|
|
6196
|
-
updateMockupIds:
|
|
6431
|
+
updateMockupIds: updateMockupIdsGuarded,
|
|
6197
6432
|
updatePlacementSettings: updatePlacementSettingsWrapped,
|
|
6433
|
+
setSpreadCrops,
|
|
6198
6434
|
getMockupResultsImmediate: () => mockupResultsRef.current,
|
|
6199
6435
|
subscribeMockupResults: stableSubscribeMockupResults,
|
|
6200
6436
|
subscribeMockupResultById: stableSubscribeMockupResultById,
|
|
@@ -6229,8 +6465,9 @@ function RealtimeProvider({
|
|
|
6229
6465
|
disableRealtime,
|
|
6230
6466
|
sendCanvasBlob,
|
|
6231
6467
|
sendCanvasState,
|
|
6232
|
-
|
|
6468
|
+
updateMockupIdsGuarded,
|
|
6233
6469
|
updatePlacementSettingsWrapped,
|
|
6470
|
+
setSpreadCrops,
|
|
6234
6471
|
stableSubscribeMockupResults,
|
|
6235
6472
|
stableSubscribeMockupResultById,
|
|
6236
6473
|
notifyPendingExport,
|
|
@@ -9745,7 +9982,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
9745
9982
|
// Quick crossfade
|
|
9746
9983
|
shimmerDelay = 200,
|
|
9747
9984
|
shimmerFadeDuration = 500,
|
|
9748
|
-
darkShimmerFadeInDuration = 300
|
|
9985
|
+
darkShimmerFadeInDuration = 300,
|
|
9986
|
+
emptyText = "No artwork",
|
|
9987
|
+
errorText = "Error loading",
|
|
9988
|
+
unavailableText = "Preview unavailable"
|
|
9749
9989
|
}) {
|
|
9750
9990
|
const context = useProductOptional();
|
|
9751
9991
|
const designContext = useDesignOptional();
|
|
@@ -10190,7 +10430,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
10190
10430
|
)
|
|
10191
10431
|
}
|
|
10192
10432
|
),
|
|
10193
|
-
/* @__PURE__ */ jsx61("p", { className: "text-xs", children:
|
|
10433
|
+
/* @__PURE__ */ jsx61("p", { className: "text-xs", children: emptyText })
|
|
10194
10434
|
] })
|
|
10195
10435
|
}
|
|
10196
10436
|
);
|
|
@@ -10201,7 +10441,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
10201
10441
|
{
|
|
10202
10442
|
className: `bg-red-50 flex items-center justify-center ${className || ""}`,
|
|
10203
10443
|
style,
|
|
10204
|
-
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 }) })
|
|
10205
10445
|
}
|
|
10206
10446
|
);
|
|
10207
10447
|
}
|
|
@@ -10220,7 +10460,7 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
10220
10460
|
crossOrigin: "anonymous",
|
|
10221
10461
|
className: "absolute inset-0 w-full h-full object-cover",
|
|
10222
10462
|
draggable,
|
|
10223
|
-
src:
|
|
10463
|
+
src: renderedUrl,
|
|
10224
10464
|
loading: "eager",
|
|
10225
10465
|
fetchPriority: "high",
|
|
10226
10466
|
onClick,
|
|
@@ -10236,7 +10476,8 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
10236
10476
|
onError: () => {
|
|
10237
10477
|
if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
|
|
10238
10478
|
retriedUrlRef.current = renderedUrl;
|
|
10239
|
-
|
|
10479
|
+
void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
|
|
10480
|
+
}).then(() => setRetryNonce((n) => n + 1));
|
|
10240
10481
|
return;
|
|
10241
10482
|
}
|
|
10242
10483
|
setImgFailed(true);
|
|
@@ -10250,9 +10491,10 @@ var HeroProductImage = memo3(function HeroProductImage2({
|
|
|
10250
10491
|
);
|
|
10251
10492
|
}
|
|
10252
10493
|
}
|
|
10253
|
-
}
|
|
10494
|
+
},
|
|
10495
|
+
retryNonce
|
|
10254
10496
|
),
|
|
10255
|
-
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 }) }),
|
|
10256
10498
|
realtimeContext?.renderError && /* @__PURE__ */ jsx61(
|
|
10257
10499
|
"div",
|
|
10258
10500
|
{
|
|
@@ -11761,7 +12003,10 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
|
|
|
11761
12003
|
onZoomChange,
|
|
11762
12004
|
stickyHeroContext,
|
|
11763
12005
|
mockupWidth,
|
|
11764
|
-
onMockupUrlGenerated
|
|
12006
|
+
onMockupUrlGenerated,
|
|
12007
|
+
renderImageAction,
|
|
12008
|
+
errorText,
|
|
12009
|
+
unavailableText
|
|
11765
12010
|
}) {
|
|
11766
12011
|
const [activeIndex, setActiveIndex] = useState40(currentIndex);
|
|
11767
12012
|
const [touchStart, setTouchStart] = useState40(
|
|
@@ -12295,34 +12540,52 @@ var MobileProductCarousel = memo4(function MobileProductCarousel2({
|
|
|
12295
12540
|
children: images.map((image, index) => {
|
|
12296
12541
|
const DECODE_WINDOW = 1;
|
|
12297
12542
|
const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
|
|
12298
|
-
return /* @__PURE__ */
|
|
12543
|
+
return /* @__PURE__ */ jsxs50(
|
|
12299
12544
|
"div",
|
|
12300
12545
|
{
|
|
12301
12546
|
className: "w-full h-full flex-shrink-0 relative select-none",
|
|
12302
|
-
children:
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
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
|
+
]
|
|
12326
12589
|
},
|
|
12327
12590
|
index
|
|
12328
12591
|
);
|