@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1632](https://github.com/snowcone-app/snowcone-monorepo/pull/1632) [`8a3b7e2`](https://github.com/snowcone-app/snowcone-monorepo/commit/8a3b7e26867257a22073dc5a55fb19d94ffded3f) Thanks [@kevinsproles](https://github.com/kevinsproles)! - RealtimeProvider: decode the frozen cropped-delivery background at seed time. With ws binary frame delivery the full frame's `imageUrl` is a live `blob:` URL the SDK revokes when the next frame for that mockup arrives; lazily loading it at composite time failed (`E_OVERLAY_FRAME_LOAD`), disabled cropped delivery for the session, and cost three extra full renders per edit.
|
|
8
|
+
|
|
9
|
+
## 0.4.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`e57a5bc`](https://github.com/snowcone-app/snowcone-monorepo/commit/e57a5bcb3b62ac612e43c47ab2709992ef6db385)]:
|
|
14
|
+
- @snowcone-app/sdk@0.20.3
|
|
15
|
+
|
|
3
16
|
## 0.4.9
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
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,217 @@ 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
|
+
const imgPromise = loadImageElement(result.imageUrl);
|
|
6307
|
+
imgPromise.catch(() => {
|
|
6308
|
+
});
|
|
6309
|
+
backgroundsRef.current.set(result.mockupId, { url: result.imageUrl, imgPromise });
|
|
6310
|
+
}
|
|
6311
|
+
displayable.push(result);
|
|
6312
|
+
} else if (firstSight && sessionLive) {
|
|
6313
|
+
processedFrameRef.current.set(result.mockupId, result.imageUrl);
|
|
6314
|
+
compositeQueueRef.current.set(result.mockupId, result);
|
|
6315
|
+
pumpCompositeQueue(result.mockupId);
|
|
6316
|
+
} else if (pendingMockupIdsRef.current.has(result.mockupId)) {
|
|
6317
|
+
const cached = mockupUrlCacheRef.current.get(result.mockupId);
|
|
6318
|
+
if (cached) displayable.push({ ...result, imageUrl: cached.url });
|
|
6096
6319
|
}
|
|
6097
6320
|
});
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
}
|
|
6102
|
-
}, [rawMockupResults]);
|
|
6321
|
+
deliverResults(displayable);
|
|
6322
|
+
maybeFlipCrop();
|
|
6323
|
+
}, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
|
|
6103
6324
|
const sendCanvasBlob = (0, import_react25.useCallback)(
|
|
6104
6325
|
(placement, blob, mockupCount, baseThrottleMs) => {
|
|
6105
6326
|
blobQueuedGenRef.current = pipelineGenRef.current;
|
|
@@ -6160,9 +6381,11 @@ function RealtimeProvider({
|
|
|
6160
6381
|
lastSentBgRef.current = void 0;
|
|
6161
6382
|
lastSentSpreadCropsRef.current = "null";
|
|
6162
6383
|
lastSentColorsRef.current = {};
|
|
6384
|
+
invalidateBackgrounds();
|
|
6385
|
+
lastConfigBaseRef.current = null;
|
|
6163
6386
|
};
|
|
6164
6387
|
}
|
|
6165
|
-
}, [isEnabled, connect, disconnect, wsUrl]);
|
|
6388
|
+
}, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
|
|
6166
6389
|
(0, import_react25.useEffect)(() => {
|
|
6167
6390
|
if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
|
|
6168
6391
|
isPendingMockupsRef.current = false;
|
|
@@ -6186,6 +6409,9 @@ function RealtimeProvider({
|
|
|
6186
6409
|
if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
|
|
6187
6410
|
return;
|
|
6188
6411
|
}
|
|
6412
|
+
if (variantChanged || bgChanged) {
|
|
6413
|
+
invalidateBackgrounds();
|
|
6414
|
+
}
|
|
6189
6415
|
const mockupIds = [];
|
|
6190
6416
|
if (product.mockups && Array.isArray(product.mockups)) {
|
|
6191
6417
|
product.mockups.forEach((mockup) => {
|
|
@@ -6220,7 +6446,10 @@ function RealtimeProvider({
|
|
|
6220
6446
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
|
|
6221
6447
|
});
|
|
6222
6448
|
}
|
|
6223
|
-
|
|
6449
|
+
lastConfigBaseRef.current = configPayload;
|
|
6450
|
+
sendConfigRaw(
|
|
6451
|
+
croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
|
|
6452
|
+
);
|
|
6224
6453
|
configSentRef.current = true;
|
|
6225
6454
|
lastSentVariantIdRef.current = variantId;
|
|
6226
6455
|
lastSentBgRef.current = mockupBg;
|
|
@@ -6242,6 +6471,7 @@ function RealtimeProvider({
|
|
|
6242
6471
|
productContext?.selection,
|
|
6243
6472
|
sendConfigRaw,
|
|
6244
6473
|
sendColorBlobTracked,
|
|
6474
|
+
invalidateBackgrounds,
|
|
6245
6475
|
mockupWidth,
|
|
6246
6476
|
mockupBg,
|
|
6247
6477
|
spreadCropsVersion
|
|
@@ -6271,6 +6501,15 @@ function RealtimeProvider({
|
|
|
6271
6501
|
productContext?.selection,
|
|
6272
6502
|
sendColorBlobTracked
|
|
6273
6503
|
]);
|
|
6504
|
+
const updateMockupIdsGuarded = (0, import_react25.useCallback)(
|
|
6505
|
+
(mockupIds) => {
|
|
6506
|
+
if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
|
|
6507
|
+
unflipCrop();
|
|
6508
|
+
}
|
|
6509
|
+
return updateMockupIdsRaw(mockupIds);
|
|
6510
|
+
},
|
|
6511
|
+
[updateMockupIdsRaw, unflipCrop]
|
|
6512
|
+
);
|
|
6274
6513
|
const updatePlacementSettingsWrapped = (0, import_react25.useCallback)(
|
|
6275
6514
|
(settings) => {
|
|
6276
6515
|
placementSettingsOverridesRef.current = settings;
|
|
@@ -6470,28 +6709,6 @@ function RealtimeProvider({
|
|
|
6470
6709
|
const isMockupLoading = (0, import_react25.useCallback)((mockupId) => {
|
|
6471
6710
|
return pendingMockupIdsRef.current.has(mockupId);
|
|
6472
6711
|
}, []);
|
|
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
6712
|
const subscribeMockupResultsRef = (0, import_react25.useRef)(subscribeMockupResults);
|
|
6496
6713
|
const subscribeMockupResultByIdRef = (0, import_react25.useRef)(subscribeMockupResultById);
|
|
6497
6714
|
const subscribePendingBlobRef = (0, import_react25.useRef)(subscribePendingBlob);
|
|
@@ -6572,7 +6789,7 @@ function RealtimeProvider({
|
|
|
6572
6789
|
disableRealtime,
|
|
6573
6790
|
sendCanvasBlob,
|
|
6574
6791
|
sendCanvasState,
|
|
6575
|
-
updateMockupIds:
|
|
6792
|
+
updateMockupIds: updateMockupIdsGuarded,
|
|
6576
6793
|
updatePlacementSettings: updatePlacementSettingsWrapped,
|
|
6577
6794
|
setSpreadCrops,
|
|
6578
6795
|
getMockupResultsImmediate: () => mockupResultsRef.current,
|
|
@@ -6609,7 +6826,7 @@ function RealtimeProvider({
|
|
|
6609
6826
|
disableRealtime,
|
|
6610
6827
|
sendCanvasBlob,
|
|
6611
6828
|
sendCanvasState,
|
|
6612
|
-
|
|
6829
|
+
updateMockupIdsGuarded,
|
|
6613
6830
|
updatePlacementSettingsWrapped,
|
|
6614
6831
|
setSpreadCrops,
|
|
6615
6832
|
stableSubscribeMockupResults,
|
|
@@ -10086,7 +10303,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10086
10303
|
// Quick crossfade
|
|
10087
10304
|
shimmerDelay = 200,
|
|
10088
10305
|
shimmerFadeDuration = 500,
|
|
10089
|
-
darkShimmerFadeInDuration = 300
|
|
10306
|
+
darkShimmerFadeInDuration = 300,
|
|
10307
|
+
emptyText = "No artwork",
|
|
10308
|
+
errorText = "Error loading",
|
|
10309
|
+
unavailableText = "Preview unavailable"
|
|
10090
10310
|
}) {
|
|
10091
10311
|
const context = useProductOptional();
|
|
10092
10312
|
const designContext = useDesignOptional();
|
|
@@ -10531,7 +10751,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10531
10751
|
)
|
|
10532
10752
|
}
|
|
10533
10753
|
),
|
|
10534
|
-
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children:
|
|
10754
|
+
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children: emptyText })
|
|
10535
10755
|
] })
|
|
10536
10756
|
}
|
|
10537
10757
|
);
|
|
@@ -10542,7 +10762,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10542
10762
|
{
|
|
10543
10763
|
className: `bg-red-50 flex items-center justify-center ${className || ""}`,
|
|
10544
10764
|
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:
|
|
10765
|
+
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
10766
|
}
|
|
10547
10767
|
);
|
|
10548
10768
|
}
|
|
@@ -10561,7 +10781,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10561
10781
|
crossOrigin: "anonymous",
|
|
10562
10782
|
className: "absolute inset-0 w-full h-full object-cover",
|
|
10563
10783
|
draggable,
|
|
10564
|
-
src:
|
|
10784
|
+
src: renderedUrl,
|
|
10565
10785
|
loading: "eager",
|
|
10566
10786
|
fetchPriority: "high",
|
|
10567
10787
|
onClick,
|
|
@@ -10577,7 +10797,8 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10577
10797
|
onError: () => {
|
|
10578
10798
|
if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
|
|
10579
10799
|
retriedUrlRef.current = renderedUrl;
|
|
10580
|
-
|
|
10800
|
+
void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
|
|
10801
|
+
}).then(() => setRetryNonce((n) => n + 1));
|
|
10581
10802
|
return;
|
|
10582
10803
|
}
|
|
10583
10804
|
setImgFailed(true);
|
|
@@ -10591,9 +10812,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10591
10812
|
);
|
|
10592
10813
|
}
|
|
10593
10814
|
}
|
|
10594
|
-
}
|
|
10815
|
+
},
|
|
10816
|
+
retryNonce
|
|
10595
10817
|
),
|
|
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:
|
|
10818
|
+
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
10819
|
realtimeContext?.renderError && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10598
10820
|
"div",
|
|
10599
10821
|
{
|
|
@@ -12083,7 +12305,10 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12083
12305
|
onZoomChange,
|
|
12084
12306
|
stickyHeroContext,
|
|
12085
12307
|
mockupWidth,
|
|
12086
|
-
onMockupUrlGenerated
|
|
12308
|
+
onMockupUrlGenerated,
|
|
12309
|
+
renderImageAction,
|
|
12310
|
+
errorText,
|
|
12311
|
+
unavailableText
|
|
12087
12312
|
}) {
|
|
12088
12313
|
const [activeIndex, setActiveIndex] = (0, import_react56.useState)(currentIndex);
|
|
12089
12314
|
const [touchStart, setTouchStart] = (0, import_react56.useState)(
|
|
@@ -12617,34 +12842,52 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12617
12842
|
children: images.map((image, index) => {
|
|
12618
12843
|
const DECODE_WINDOW = 1;
|
|
12619
12844
|
const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
|
|
12620
|
-
return /* @__PURE__ */ (0, import_jsx_runtime74.
|
|
12845
|
+
return /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)(
|
|
12621
12846
|
"div",
|
|
12622
12847
|
{
|
|
12623
12848
|
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
|
-
|
|
12849
|
+
children: [
|
|
12850
|
+
!isWithinDecodeWindow ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)("div", { className: "w-full h-full bg-muted" }) : image.isPlaceholder ? (
|
|
12851
|
+
// Rainbow prism loader (same as the /create route) instead of
|
|
12852
|
+
// a flat pulse; fills this relative slide via inset:0.
|
|
12853
|
+
/* @__PURE__ */ (0, import_jsx_runtime74.jsx)(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" })
|
|
12854
|
+
) : image.isRealMockup && currentArtwork ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12855
|
+
HeroProductImage,
|
|
12856
|
+
{
|
|
12857
|
+
productId,
|
|
12858
|
+
artwork: currentArtwork,
|
|
12859
|
+
placement: image.placement,
|
|
12860
|
+
mockupId: image.mockupId,
|
|
12861
|
+
width: mockupWidth,
|
|
12862
|
+
realtimeUrl: image.mockupId ? realtimeMockupUrls[image.mockupId] ?? generatedUrlsRef.current.get(image.mockupId) ?? void 0 : void 0,
|
|
12863
|
+
onUrlGenerated: image.mockupId ? (url) => handleUrlGenerated(image.mockupId, url) : void 0,
|
|
12864
|
+
errorText,
|
|
12865
|
+
unavailableText,
|
|
12866
|
+
className: "w-full h-full object-cover cursor-pointer pointer-events-none"
|
|
12867
|
+
}
|
|
12868
|
+
) : image.isRealMockup && !currentArtwork ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(LoadingOverlayPrismCandyInline, { visible: true, variant: "light" }) : /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12869
|
+
StaticCarouselImage,
|
|
12870
|
+
{
|
|
12871
|
+
src: image.src,
|
|
12872
|
+
alt: image.label ?? "",
|
|
12873
|
+
className: "w-full h-full object-cover cursor-pointer pointer-events-none"
|
|
12874
|
+
}
|
|
12875
|
+
),
|
|
12876
|
+
isWithinDecodeWindow && (() => {
|
|
12877
|
+
const action = renderImageAction?.(image, index);
|
|
12878
|
+
return action ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
|
|
12879
|
+
"div",
|
|
12880
|
+
{
|
|
12881
|
+
className: "absolute top-3 right-3 z-10",
|
|
12882
|
+
onClick: (e) => e.stopPropagation(),
|
|
12883
|
+
onTouchStart: (e) => e.stopPropagation(),
|
|
12884
|
+
onTouchEnd: (e) => e.stopPropagation(),
|
|
12885
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
12886
|
+
children: action
|
|
12887
|
+
}
|
|
12888
|
+
) : null;
|
|
12889
|
+
})()
|
|
12890
|
+
]
|
|
12648
12891
|
},
|
|
12649
12892
|
index
|
|
12650
12893
|
);
|