@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/CHANGELOG.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -5929,6 +5929,67 @@ function useMockupBackgroundOptional() {
|
|
|
5929
5929
|
return (0, import_react24.useContext)(MockupBackgroundContext);
|
|
5930
5930
|
}
|
|
5931
5931
|
|
|
5932
|
+
// src/patterns/croppedFrameComposite.ts
|
|
5933
|
+
var COMPOSITE_QUALITY = 0.92;
|
|
5934
|
+
function isFullRect(rect) {
|
|
5935
|
+
if (!rect) return true;
|
|
5936
|
+
return rect.x === 0 && rect.y === 0 && rect.w === 1 && rect.h === 1;
|
|
5937
|
+
}
|
|
5938
|
+
function compositeOffset(rect, bgWidth, bgHeight) {
|
|
5939
|
+
return {
|
|
5940
|
+
dx: Math.round(rect.x * bgWidth),
|
|
5941
|
+
dy: Math.round(rect.y * bgHeight)
|
|
5942
|
+
};
|
|
5943
|
+
}
|
|
5944
|
+
function allBackgroundsSeeded(mockupIds, seeded) {
|
|
5945
|
+
return mockupIds.length > 0 && mockupIds.every((id) => seeded.has(id));
|
|
5946
|
+
}
|
|
5947
|
+
function loadImageElement(url) {
|
|
5948
|
+
return new Promise((resolve, reject) => {
|
|
5949
|
+
const img = new Image();
|
|
5950
|
+
img.crossOrigin = "anonymous";
|
|
5951
|
+
img.onload = () => {
|
|
5952
|
+
if (img.decode) {
|
|
5953
|
+
img.decode().then(
|
|
5954
|
+
() => resolve(img),
|
|
5955
|
+
() => resolve(img)
|
|
5956
|
+
);
|
|
5957
|
+
} else {
|
|
5958
|
+
resolve(img);
|
|
5959
|
+
}
|
|
5960
|
+
};
|
|
5961
|
+
img.onerror = () => reject(
|
|
5962
|
+
new Error(`E_OVERLAY_FRAME_LOAD: image failed to load: ${url.slice(0, 160)}`)
|
|
5963
|
+
);
|
|
5964
|
+
img.src = url;
|
|
5965
|
+
});
|
|
5966
|
+
}
|
|
5967
|
+
async function compositeCroppedFrame(args) {
|
|
5968
|
+
const frame = await loadImageElement(args.frameUrl);
|
|
5969
|
+
const width = args.background.naturalWidth;
|
|
5970
|
+
const height = args.background.naturalHeight;
|
|
5971
|
+
if (!width || !height) {
|
|
5972
|
+
throw new Error("E_OVERLAY_BG_EMPTY: background image has no pixels");
|
|
5973
|
+
}
|
|
5974
|
+
const canvas = args.canvas ?? document.createElement("canvas");
|
|
5975
|
+
canvas.width = width;
|
|
5976
|
+
canvas.height = height;
|
|
5977
|
+
const ctx = canvas.getContext("2d");
|
|
5978
|
+
if (!ctx) {
|
|
5979
|
+
throw new Error("E_OVERLAY_NO_2D_CONTEXT: canvas 2d context unavailable");
|
|
5980
|
+
}
|
|
5981
|
+
ctx.drawImage(args.background, 0, 0);
|
|
5982
|
+
const { dx, dy } = compositeOffset(args.rect, width, height);
|
|
5983
|
+
ctx.drawImage(frame, dx, dy);
|
|
5984
|
+
const blob = await new Promise(
|
|
5985
|
+
(resolve) => canvas.toBlob(resolve, "image/webp", COMPOSITE_QUALITY)
|
|
5986
|
+
);
|
|
5987
|
+
if (!blob) {
|
|
5988
|
+
throw new Error("E_OVERLAY_ENCODE_FAILED: canvas.toBlob returned null");
|
|
5989
|
+
}
|
|
5990
|
+
return blob;
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5932
5993
|
// src/patterns/RealtimeProvider.tsx
|
|
5933
5994
|
var import_jsx_runtime52 = require("react/jsx-runtime");
|
|
5934
5995
|
var DEFAULT_ARTBOARD_SIZE = 2e3;
|
|
@@ -5944,7 +6005,8 @@ function RealtimeProvider({
|
|
|
5944
6005
|
mockupWidth = 1400,
|
|
5945
6006
|
autoEnable = false,
|
|
5946
6007
|
shop,
|
|
5947
|
-
grantUrl
|
|
6008
|
+
grantUrl,
|
|
6009
|
+
delivery = "cropped"
|
|
5948
6010
|
}) {
|
|
5949
6011
|
const productContext = useProductOptional();
|
|
5950
6012
|
const mockupBg = useMockupBackgroundOptional();
|
|
@@ -6048,58 +6110,214 @@ function RealtimeProvider({
|
|
|
6048
6110
|
const registeredBlobUrlsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6049
6111
|
const pendingPlacementsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6050
6112
|
const pendingMockupIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6051
|
-
(0, import_react25.
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6113
|
+
const backgroundsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6114
|
+
const croppedActiveRef = (0, import_react25.useRef)(false);
|
|
6115
|
+
const cropCapableRef = (0, import_react25.useRef)(true);
|
|
6116
|
+
const lastConfigBaseRef = (0, import_react25.useRef)(null);
|
|
6117
|
+
const sceneGenRef = (0, import_react25.useRef)(0);
|
|
6118
|
+
const processedFrameRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6119
|
+
const compositeQueueRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6120
|
+
const compositeBusyRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6121
|
+
const compositeCanvasRef = (0, import_react25.useRef)(null);
|
|
6122
|
+
const registerBlobUrl = (0, import_react25.useCallback)((blobOrUrl, placementName) => {
|
|
6123
|
+
let url;
|
|
6124
|
+
if (blobOrUrl instanceof Blob) {
|
|
6125
|
+
if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
|
|
6126
|
+
const oldest = registeredBlobUrlsRef.current.values().next().value;
|
|
6127
|
+
if (oldest) {
|
|
6128
|
+
URL.revokeObjectURL(oldest);
|
|
6129
|
+
registeredBlobUrlsRef.current.delete(oldest);
|
|
6130
|
+
mockupUrlCacheRef.current.forEach((value, key) => {
|
|
6131
|
+
if (value.url === oldest) {
|
|
6132
|
+
mockupUrlCacheRef.current.delete(key);
|
|
6133
|
+
}
|
|
6134
|
+
});
|
|
6135
|
+
mockupResultsRef.current = mockupResultsRef.current.filter(
|
|
6136
|
+
(r) => r.imageUrl !== oldest
|
|
6137
|
+
);
|
|
6138
|
+
}
|
|
6056
6139
|
}
|
|
6057
|
-
|
|
6140
|
+
url = URL.createObjectURL(blobOrUrl);
|
|
6141
|
+
registeredBlobUrlsRef.current.add(url);
|
|
6142
|
+
} else {
|
|
6143
|
+
url = blobOrUrl;
|
|
6058
6144
|
}
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6145
|
+
return url;
|
|
6146
|
+
}, []);
|
|
6147
|
+
const croppedDeliveryEnabled = delivery !== "full-frame";
|
|
6148
|
+
const sendCropConfig = (0, import_react25.useCallback)(
|
|
6149
|
+
(withCrop) => {
|
|
6150
|
+
const base = lastConfigBaseRef.current;
|
|
6151
|
+
if (!base) return;
|
|
6152
|
+
croppedActiveRef.current = withCrop;
|
|
6153
|
+
sendConfigRaw({
|
|
6154
|
+
...base,
|
|
6155
|
+
placementSettings: { ...placementSettingsOverridesRef.current },
|
|
6156
|
+
...withCrop ? { overlayCrop: "auto" } : {}
|
|
6157
|
+
});
|
|
6158
|
+
},
|
|
6159
|
+
[sendConfigRaw]
|
|
6160
|
+
);
|
|
6161
|
+
const unflipCrop = (0, import_react25.useCallback)(() => {
|
|
6162
|
+
if (!croppedActiveRef.current) return;
|
|
6163
|
+
sendCropConfig(false);
|
|
6164
|
+
}, [sendCropConfig]);
|
|
6165
|
+
const maybeFlipCrop = (0, import_react25.useCallback)(() => {
|
|
6166
|
+
if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
|
|
6167
|
+
if (croppedActiveRef.current) return;
|
|
6168
|
+
const base = lastConfigBaseRef.current;
|
|
6169
|
+
if (!base) return;
|
|
6170
|
+
const ids = base.mockupIds ?? [];
|
|
6171
|
+
const seeded = new Set(backgroundsRef.current.keys());
|
|
6172
|
+
if (allBackgroundsSeeded(ids, seeded)) {
|
|
6173
|
+
console.log(`[RTP] Cropped delivery ON \u2014 all ${ids.length} mockup background(s) seeded`);
|
|
6174
|
+
sendCropConfig(true);
|
|
6175
|
+
} else {
|
|
6176
|
+
const missing = ids.filter((id) => !seeded.has(id));
|
|
6177
|
+
console.log(`[RTP] Cropped delivery waiting \u2014 ${missing.length}/${ids.length} mockup(s) unseeded: [${missing.slice(0, 8).join(", ")}]`);
|
|
6178
|
+
}
|
|
6179
|
+
}, [croppedDeliveryEnabled, sendCropConfig]);
|
|
6180
|
+
const invalidateBackgrounds = (0, import_react25.useCallback)(() => {
|
|
6181
|
+
sceneGenRef.current += 1;
|
|
6182
|
+
backgroundsRef.current.clear();
|
|
6183
|
+
processedFrameRef.current.clear();
|
|
6184
|
+
compositeQueueRef.current.clear();
|
|
6185
|
+
croppedActiveRef.current = false;
|
|
6186
|
+
}, []);
|
|
6187
|
+
const deliverResults = (0, import_react25.useCallback)(
|
|
6188
|
+
(results) => {
|
|
6189
|
+
if (results.length === 0) return;
|
|
6190
|
+
const now = Date.now();
|
|
6191
|
+
const merged = [...mockupResultsRef.current];
|
|
6192
|
+
results.forEach((result) => {
|
|
6193
|
+
const idx = merged.findIndex((r) => r.mockupId === result.mockupId);
|
|
6194
|
+
if (idx >= 0) merged[idx] = result;
|
|
6195
|
+
else merged.push(result);
|
|
6196
|
+
});
|
|
6197
|
+
mockupResultsRef.current = merged;
|
|
6198
|
+
const clearedPlacements = /* @__PURE__ */ new Set();
|
|
6199
|
+
results.forEach((result) => {
|
|
6064
6200
|
const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
|
|
6065
6201
|
mockupUrlCacheRef.current.set(result.mockupId, {
|
|
6066
6202
|
url: result.imageUrl,
|
|
6067
|
-
isBlob:
|
|
6068
|
-
// Server URLs are not blob URLs
|
|
6203
|
+
isBlob: result.imageUrl.startsWith("blob:"),
|
|
6069
6204
|
timestamp: now
|
|
6070
6205
|
});
|
|
6071
6206
|
pendingMockupIdsRef.current.delete(result.mockupId);
|
|
6072
6207
|
if (wasPending) {
|
|
6073
6208
|
console.log(`[RTP] Cleared pending for ${result.mockupId} \u2014 url cached`);
|
|
6074
6209
|
}
|
|
6210
|
+
if (result.placement) clearedPlacements.add(result.placement);
|
|
6211
|
+
});
|
|
6212
|
+
if (clearedPlacements.size > 0) {
|
|
6213
|
+
clearedPlacements.forEach((p) => pendingPlacementsRef.current.delete(p));
|
|
6075
6214
|
} else {
|
|
6076
|
-
|
|
6215
|
+
pendingPlacementsRef.current.clear();
|
|
6077
6216
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6217
|
+
mockupSubscribersRef.current.forEach((cb) => cb(results));
|
|
6218
|
+
results.forEach((result) => {
|
|
6219
|
+
const subscribers = mockupSubscribersByIdRef.current.get(result.mockupId);
|
|
6220
|
+
if (subscribers && subscribers.size > 0) {
|
|
6221
|
+
console.log(`[RTP] Notifying ${subscribers.size} subscriber(s) for ${result.mockupId}`);
|
|
6222
|
+
subscribers.forEach((cb) => cb(result));
|
|
6223
|
+
} else {
|
|
6224
|
+
console.log(`[RTP] No subscribers for ${result.mockupId} \u2014 result received but nobody listening`);
|
|
6225
|
+
}
|
|
6226
|
+
});
|
|
6227
|
+
if (hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
|
|
6228
|
+
hasBlobInFlightRef.current = false;
|
|
6229
|
+
pipelineSettledSubscribersRef.current.forEach((cb) => cb());
|
|
6230
|
+
}
|
|
6231
|
+
},
|
|
6232
|
+
[hasPendingBlobs]
|
|
6233
|
+
);
|
|
6234
|
+
const runComposite = (0, import_react25.useCallback)(
|
|
6235
|
+
async (result) => {
|
|
6236
|
+
const gen = sceneGenRef.current;
|
|
6237
|
+
const bg = backgroundsRef.current.get(result.mockupId);
|
|
6238
|
+
if (!bg) {
|
|
6239
|
+
console.error(
|
|
6240
|
+
`[RealtimeProvider] E_OVERLAY_NO_BACKGROUND: cropped frame for ${result.mockupId} has no frozen background \u2014 falling back to full-frame and re-rendering it.`
|
|
6241
|
+
);
|
|
6242
|
+
unflipCrop();
|
|
6243
|
+
updateMockupIdsRaw([result.mockupId]);
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6246
|
+
try {
|
|
6247
|
+
if (!bg.imgPromise) bg.imgPromise = loadImageElement(bg.url);
|
|
6248
|
+
const background = await bg.imgPromise;
|
|
6249
|
+
if (!compositeCanvasRef.current && typeof document !== "undefined") {
|
|
6250
|
+
compositeCanvasRef.current = document.createElement("canvas");
|
|
6251
|
+
}
|
|
6252
|
+
const blob = await compositeCroppedFrame({
|
|
6253
|
+
background,
|
|
6254
|
+
frameUrl: result.imageUrl,
|
|
6255
|
+
rect: result.cropRect,
|
|
6256
|
+
canvas: compositeCanvasRef.current ?? void 0
|
|
6257
|
+
});
|
|
6258
|
+
if (gen !== sceneGenRef.current) return;
|
|
6259
|
+
deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
|
|
6260
|
+
} catch (err) {
|
|
6261
|
+
console.error(
|
|
6262
|
+
"[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
|
|
6263
|
+
err
|
|
6264
|
+
);
|
|
6265
|
+
cropCapableRef.current = false;
|
|
6266
|
+
unflipCrop();
|
|
6267
|
+
updateMockupIdsRaw([result.mockupId]);
|
|
6268
|
+
}
|
|
6269
|
+
},
|
|
6270
|
+
[deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
|
|
6271
|
+
);
|
|
6272
|
+
const pumpCompositeQueue = (0, import_react25.useCallback)(
|
|
6273
|
+
(mockupId) => {
|
|
6274
|
+
if (compositeBusyRef.current.has(mockupId)) return;
|
|
6275
|
+
const job = compositeQueueRef.current.get(mockupId);
|
|
6276
|
+
if (!job) return;
|
|
6277
|
+
compositeQueueRef.current.delete(mockupId);
|
|
6278
|
+
compositeBusyRef.current.add(mockupId);
|
|
6279
|
+
void runComposite(job).finally(() => {
|
|
6280
|
+
compositeBusyRef.current.delete(mockupId);
|
|
6281
|
+
pumpCompositeQueue(mockupId);
|
|
6282
|
+
});
|
|
6283
|
+
},
|
|
6284
|
+
[runComposite]
|
|
6285
|
+
);
|
|
6286
|
+
(0, import_react25.useEffect)(() => {
|
|
6287
|
+
if (rawMockupResults.length === 0) {
|
|
6288
|
+
if (mockupResultsRef.current.length > 0) {
|
|
6289
|
+
mockupResultsRef.current = [];
|
|
6290
|
+
mockupSubscribersRef.current.forEach((cb) => cb([]));
|
|
6291
|
+
}
|
|
6292
|
+
return;
|
|
6087
6293
|
}
|
|
6088
|
-
|
|
6294
|
+
console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
|
|
6295
|
+
const displayable = [];
|
|
6089
6296
|
rawMockupResults.forEach((result) => {
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6297
|
+
if (!result.imageUrl) {
|
|
6298
|
+
console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
|
|
6299
|
+
return;
|
|
6300
|
+
}
|
|
6301
|
+
const firstSight = processedFrameRef.current.get(result.mockupId) !== result.imageUrl;
|
|
6302
|
+
const sessionLive = configSentRef.current;
|
|
6303
|
+
if (isFullRect(result.cropRect)) {
|
|
6304
|
+
if (firstSight && sessionLive) {
|
|
6305
|
+
processedFrameRef.current.set(result.mockupId, result.imageUrl);
|
|
6306
|
+
backgroundsRef.current.set(result.mockupId, { url: result.imageUrl });
|
|
6307
|
+
}
|
|
6308
|
+
displayable.push(result);
|
|
6309
|
+
} else if (firstSight && sessionLive) {
|
|
6310
|
+
processedFrameRef.current.set(result.mockupId, result.imageUrl);
|
|
6311
|
+
compositeQueueRef.current.set(result.mockupId, result);
|
|
6312
|
+
pumpCompositeQueue(result.mockupId);
|
|
6313
|
+
} else if (pendingMockupIdsRef.current.has(result.mockupId)) {
|
|
6314
|
+
const cached = mockupUrlCacheRef.current.get(result.mockupId);
|
|
6315
|
+
if (cached) displayable.push({ ...result, imageUrl: cached.url });
|
|
6096
6316
|
}
|
|
6097
6317
|
});
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
}
|
|
6102
|
-
}, [rawMockupResults]);
|
|
6318
|
+
deliverResults(displayable);
|
|
6319
|
+
maybeFlipCrop();
|
|
6320
|
+
}, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
|
|
6103
6321
|
const sendCanvasBlob = (0, import_react25.useCallback)(
|
|
6104
6322
|
(placement, blob, mockupCount, baseThrottleMs) => {
|
|
6105
6323
|
blobQueuedGenRef.current = pipelineGenRef.current;
|
|
@@ -6160,9 +6378,11 @@ function RealtimeProvider({
|
|
|
6160
6378
|
lastSentBgRef.current = void 0;
|
|
6161
6379
|
lastSentSpreadCropsRef.current = "null";
|
|
6162
6380
|
lastSentColorsRef.current = {};
|
|
6381
|
+
invalidateBackgrounds();
|
|
6382
|
+
lastConfigBaseRef.current = null;
|
|
6163
6383
|
};
|
|
6164
6384
|
}
|
|
6165
|
-
}, [isEnabled, connect, disconnect, wsUrl]);
|
|
6385
|
+
}, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
|
|
6166
6386
|
(0, import_react25.useEffect)(() => {
|
|
6167
6387
|
if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
|
|
6168
6388
|
isPendingMockupsRef.current = false;
|
|
@@ -6186,6 +6406,9 @@ function RealtimeProvider({
|
|
|
6186
6406
|
if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
|
|
6187
6407
|
return;
|
|
6188
6408
|
}
|
|
6409
|
+
if (variantChanged || bgChanged) {
|
|
6410
|
+
invalidateBackgrounds();
|
|
6411
|
+
}
|
|
6189
6412
|
const mockupIds = [];
|
|
6190
6413
|
if (product.mockups && Array.isArray(product.mockups)) {
|
|
6191
6414
|
product.mockups.forEach((mockup) => {
|
|
@@ -6220,7 +6443,10 @@ function RealtimeProvider({
|
|
|
6220
6443
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
|
|
6221
6444
|
});
|
|
6222
6445
|
}
|
|
6223
|
-
|
|
6446
|
+
lastConfigBaseRef.current = configPayload;
|
|
6447
|
+
sendConfigRaw(
|
|
6448
|
+
croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
|
|
6449
|
+
);
|
|
6224
6450
|
configSentRef.current = true;
|
|
6225
6451
|
lastSentVariantIdRef.current = variantId;
|
|
6226
6452
|
lastSentBgRef.current = mockupBg;
|
|
@@ -6242,6 +6468,7 @@ function RealtimeProvider({
|
|
|
6242
6468
|
productContext?.selection,
|
|
6243
6469
|
sendConfigRaw,
|
|
6244
6470
|
sendColorBlobTracked,
|
|
6471
|
+
invalidateBackgrounds,
|
|
6245
6472
|
mockupWidth,
|
|
6246
6473
|
mockupBg,
|
|
6247
6474
|
spreadCropsVersion
|
|
@@ -6271,6 +6498,15 @@ function RealtimeProvider({
|
|
|
6271
6498
|
productContext?.selection,
|
|
6272
6499
|
sendColorBlobTracked
|
|
6273
6500
|
]);
|
|
6501
|
+
const updateMockupIdsGuarded = (0, import_react25.useCallback)(
|
|
6502
|
+
(mockupIds) => {
|
|
6503
|
+
if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
|
|
6504
|
+
unflipCrop();
|
|
6505
|
+
}
|
|
6506
|
+
return updateMockupIdsRaw(mockupIds);
|
|
6507
|
+
},
|
|
6508
|
+
[updateMockupIdsRaw, unflipCrop]
|
|
6509
|
+
);
|
|
6274
6510
|
const updatePlacementSettingsWrapped = (0, import_react25.useCallback)(
|
|
6275
6511
|
(settings) => {
|
|
6276
6512
|
placementSettingsOverridesRef.current = settings;
|
|
@@ -6470,28 +6706,6 @@ function RealtimeProvider({
|
|
|
6470
6706
|
const isMockupLoading = (0, import_react25.useCallback)((mockupId) => {
|
|
6471
6707
|
return pendingMockupIdsRef.current.has(mockupId);
|
|
6472
6708
|
}, []);
|
|
6473
|
-
const registerBlobUrl = (0, import_react25.useCallback)((blobOrUrl, placementName) => {
|
|
6474
|
-
let url;
|
|
6475
|
-
if (blobOrUrl instanceof Blob) {
|
|
6476
|
-
if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
|
|
6477
|
-
const oldest = registeredBlobUrlsRef.current.values().next().value;
|
|
6478
|
-
if (oldest) {
|
|
6479
|
-
URL.revokeObjectURL(oldest);
|
|
6480
|
-
registeredBlobUrlsRef.current.delete(oldest);
|
|
6481
|
-
mockupUrlCacheRef.current.forEach((value, key) => {
|
|
6482
|
-
if (value.url === oldest) {
|
|
6483
|
-
mockupUrlCacheRef.current.delete(key);
|
|
6484
|
-
}
|
|
6485
|
-
});
|
|
6486
|
-
}
|
|
6487
|
-
}
|
|
6488
|
-
url = URL.createObjectURL(blobOrUrl);
|
|
6489
|
-
registeredBlobUrlsRef.current.add(url);
|
|
6490
|
-
} else {
|
|
6491
|
-
url = blobOrUrl;
|
|
6492
|
-
}
|
|
6493
|
-
return url;
|
|
6494
|
-
}, []);
|
|
6495
6709
|
const subscribeMockupResultsRef = (0, import_react25.useRef)(subscribeMockupResults);
|
|
6496
6710
|
const subscribeMockupResultByIdRef = (0, import_react25.useRef)(subscribeMockupResultById);
|
|
6497
6711
|
const subscribePendingBlobRef = (0, import_react25.useRef)(subscribePendingBlob);
|
|
@@ -6572,7 +6786,7 @@ function RealtimeProvider({
|
|
|
6572
6786
|
disableRealtime,
|
|
6573
6787
|
sendCanvasBlob,
|
|
6574
6788
|
sendCanvasState,
|
|
6575
|
-
updateMockupIds:
|
|
6789
|
+
updateMockupIds: updateMockupIdsGuarded,
|
|
6576
6790
|
updatePlacementSettings: updatePlacementSettingsWrapped,
|
|
6577
6791
|
setSpreadCrops,
|
|
6578
6792
|
getMockupResultsImmediate: () => mockupResultsRef.current,
|
|
@@ -6609,7 +6823,7 @@ function RealtimeProvider({
|
|
|
6609
6823
|
disableRealtime,
|
|
6610
6824
|
sendCanvasBlob,
|
|
6611
6825
|
sendCanvasState,
|
|
6612
|
-
|
|
6826
|
+
updateMockupIdsGuarded,
|
|
6613
6827
|
updatePlacementSettingsWrapped,
|
|
6614
6828
|
setSpreadCrops,
|
|
6615
6829
|
stableSubscribeMockupResults,
|
|
@@ -10086,7 +10300,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10086
10300
|
// Quick crossfade
|
|
10087
10301
|
shimmerDelay = 200,
|
|
10088
10302
|
shimmerFadeDuration = 500,
|
|
10089
|
-
darkShimmerFadeInDuration = 300
|
|
10303
|
+
darkShimmerFadeInDuration = 300,
|
|
10304
|
+
emptyText = "No artwork",
|
|
10305
|
+
errorText = "Error loading",
|
|
10306
|
+
unavailableText = "Preview unavailable"
|
|
10090
10307
|
}) {
|
|
10091
10308
|
const context = useProductOptional();
|
|
10092
10309
|
const designContext = useDesignOptional();
|
|
@@ -10531,7 +10748,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10531
10748
|
)
|
|
10532
10749
|
}
|
|
10533
10750
|
),
|
|
10534
|
-
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children:
|
|
10751
|
+
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children: emptyText })
|
|
10535
10752
|
] })
|
|
10536
10753
|
}
|
|
10537
10754
|
);
|
|
@@ -10542,7 +10759,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10542
10759
|
{
|
|
10543
10760
|
className: `bg-red-50 flex items-center justify-center ${className || ""}`,
|
|
10544
10761
|
style,
|
|
10545
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children:
|
|
10762
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("div", { className: "text-red-600 text-center p-4", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children: errorText }) })
|
|
10546
10763
|
}
|
|
10547
10764
|
);
|
|
10548
10765
|
}
|
|
@@ -10561,7 +10778,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10561
10778
|
crossOrigin: "anonymous",
|
|
10562
10779
|
className: "absolute inset-0 w-full h-full object-cover",
|
|
10563
10780
|
draggable,
|
|
10564
|
-
src:
|
|
10781
|
+
src: renderedUrl,
|
|
10565
10782
|
loading: "eager",
|
|
10566
10783
|
fetchPriority: "high",
|
|
10567
10784
|
onClick,
|
|
@@ -10577,7 +10794,8 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10577
10794
|
onError: () => {
|
|
10578
10795
|
if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
|
|
10579
10796
|
retriedUrlRef.current = renderedUrl;
|
|
10580
|
-
|
|
10797
|
+
void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
|
|
10798
|
+
}).then(() => setRetryNonce((n) => n + 1));
|
|
10581
10799
|
return;
|
|
10582
10800
|
}
|
|
10583
10801
|
setImgFailed(true);
|
|
@@ -10591,9 +10809,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10591
10809
|
);
|
|
10592
10810
|
}
|
|
10593
10811
|
}
|
|
10594
|
-
}
|
|
10812
|
+
},
|
|
10813
|
+
retryNonce
|
|
10595
10814
|
),
|
|
10596
|
-
imgFailed && !prevUrl && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-muted-foreground text-xs", children:
|
|
10815
|
+
imgFailed && !prevUrl && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-muted-foreground text-xs", children: unavailableText }) }),
|
|
10597
10816
|
realtimeContext?.renderError && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10598
10817
|
"div",
|
|
10599
10818
|
{
|
|
@@ -12083,7 +12302,10 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12083
12302
|
onZoomChange,
|
|
12084
12303
|
stickyHeroContext,
|
|
12085
12304
|
mockupWidth,
|
|
12086
|
-
onMockupUrlGenerated
|
|
12305
|
+
onMockupUrlGenerated,
|
|
12306
|
+
renderImageAction,
|
|
12307
|
+
errorText,
|
|
12308
|
+
unavailableText
|
|
12087
12309
|
}) {
|
|
12088
12310
|
const [activeIndex, setActiveIndex] = (0, import_react56.useState)(currentIndex);
|
|
12089
12311
|
const [touchStart, setTouchStart] = (0, import_react56.useState)(
|
|
@@ -12617,34 +12839,52 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12617
12839
|
children: images.map((image, index) => {
|
|
12618
12840
|
const DECODE_WINDOW = 1;
|
|
12619
12841
|
const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
|
|
12620
|
-
return /* @__PURE__ */ (0, import_jsx_runtime74.
|
|
12842
|
+
return /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)(
|
|
12621
12843
|
"div",
|
|
12622
12844
|
{
|
|
12623
12845
|
className: "w-full h-full flex-shrink-0 relative select-none",
|
|
12624
|
-
children:
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12846
|
+
children: [
|
|
12847
|
+
!isWithinDecodeWindow ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)("div", { className: "w-full h-full bg-muted" }) : image.isPlaceholder ? (
|
|
12848
|
+
// Rainbow prism loader (same as the /create route) instead of
|
|
12849
|
+
// a flat pulse; fills this relative slide via inset:0.
|
|
12850
|
+
/* @__PURE__ */ (0, import_jsx_runtime74.jsx)(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" })
|
|
12851
|
+
) : image.isRealMockup && currentArtwork ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12852
|
+
HeroProductImage,
|
|
12853
|
+
{
|
|
12854
|
+
productId,
|
|
12855
|
+
artwork: currentArtwork,
|
|
12856
|
+
placement: image.placement,
|
|
12857
|
+
mockupId: image.mockupId,
|
|
12858
|
+
width: mockupWidth,
|
|
12859
|
+
realtimeUrl: image.mockupId ? realtimeMockupUrls[image.mockupId] ?? generatedUrlsRef.current.get(image.mockupId) ?? void 0 : void 0,
|
|
12860
|
+
onUrlGenerated: image.mockupId ? (url) => handleUrlGenerated(image.mockupId, url) : void 0,
|
|
12861
|
+
errorText,
|
|
12862
|
+
unavailableText,
|
|
12863
|
+
className: "w-full h-full object-cover cursor-pointer pointer-events-none"
|
|
12864
|
+
}
|
|
12865
|
+
) : image.isRealMockup && !currentArtwork ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" }) : /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12866
|
+
StaticCarouselImage,
|
|
12867
|
+
{
|
|
12868
|
+
src: image.src,
|
|
12869
|
+
alt: image.label ?? "",
|
|
12870
|
+
className: "w-full h-full object-cover cursor-pointer pointer-events-none"
|
|
12871
|
+
}
|
|
12872
|
+
),
|
|
12873
|
+
isWithinDecodeWindow && (() => {
|
|
12874
|
+
const action = renderImageAction?.(image, index);
|
|
12875
|
+
return action ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12876
|
+
"div",
|
|
12877
|
+
{
|
|
12878
|
+
className: "absolute top-3 right-3 z-10",
|
|
12879
|
+
onClick: (e) => e.stopPropagation(),
|
|
12880
|
+
onTouchStart: (e) => e.stopPropagation(),
|
|
12881
|
+
onTouchEnd: (e) => e.stopPropagation(),
|
|
12882
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
12883
|
+
children: action
|
|
12884
|
+
}
|
|
12885
|
+
) : null;
|
|
12886
|
+
})()
|
|
12887
|
+
]
|
|
12648
12888
|
},
|
|
12649
12889
|
index
|
|
12650
12890
|
);
|