@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`e57a5bc`](https://github.com/snowcone-app/snowcone-monorepo/commit/e57a5bcb3b62ac612e43c47ab2709992ef6db385)]:
|
|
8
|
+
- @snowcone-app/sdk@0.20.3
|
|
9
|
+
|
|
10
|
+
## 0.4.9
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- [#1150](https://github.com/snowcone-app/snowcone-monorepo/pull/1150) [`06997b2`](https://github.com/snowcone-app/snowcone-monorepo/commit/06997b211736d906d16eb9edeb186404f5acb19c) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Spread-mode printables now use JSON canvas-state realtime mode: `WebSocketConfig.spreadCrops` carries the spread/group artboard geometry (piece rects keyed by placement label), and `RealtimeProvider.setSpreadCrops` sends it on the realtime config so the server slices each rendered spread artboard into per-placement images — no client pixel upload.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [[`06997b2`](https://github.com/snowcone-app/snowcone-monorepo/commit/06997b211736d906d16eb9edeb186404f5acb19c)]:
|
|
17
|
+
- @snowcone-app/sdk@0.20.2
|
|
18
|
+
|
|
3
19
|
## 0.4.8
|
|
4
20
|
|
|
5
21
|
### 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();
|
|
@@ -5969,6 +6031,9 @@ function RealtimeProvider({
|
|
|
5969
6031
|
const placementSettingsOverridesRef = (0, import_react25.useRef)({});
|
|
5970
6032
|
const lastSentVariantIdRef = (0, import_react25.useRef)(null);
|
|
5971
6033
|
const lastSentBgRef = (0, import_react25.useRef)(void 0);
|
|
6034
|
+
const spreadCropsRef = (0, import_react25.useRef)(void 0);
|
|
6035
|
+
const [spreadCropsVersion, setSpreadCropsVersion] = (0, import_react25.useState)(0);
|
|
6036
|
+
const lastSentSpreadCropsRef = (0, import_react25.useRef)("null");
|
|
5972
6037
|
const lastSentColorsRef = (0, import_react25.useRef)({});
|
|
5973
6038
|
const realtimeMockup = (0, import_react26.useRealtimeMockup)({
|
|
5974
6039
|
wsUrl,
|
|
@@ -6045,58 +6110,214 @@ function RealtimeProvider({
|
|
|
6045
6110
|
const registeredBlobUrlsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6046
6111
|
const pendingPlacementsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6047
6112
|
const pendingMockupIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6048
|
-
(0, import_react25.
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
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
|
+
}
|
|
6053
6139
|
}
|
|
6054
|
-
|
|
6140
|
+
url = URL.createObjectURL(blobOrUrl);
|
|
6141
|
+
registeredBlobUrlsRef.current.add(url);
|
|
6142
|
+
} else {
|
|
6143
|
+
url = blobOrUrl;
|
|
6055
6144
|
}
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
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) => {
|
|
6061
6200
|
const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
|
|
6062
6201
|
mockupUrlCacheRef.current.set(result.mockupId, {
|
|
6063
6202
|
url: result.imageUrl,
|
|
6064
|
-
isBlob:
|
|
6065
|
-
// Server URLs are not blob URLs
|
|
6203
|
+
isBlob: result.imageUrl.startsWith("blob:"),
|
|
6066
6204
|
timestamp: now
|
|
6067
6205
|
});
|
|
6068
6206
|
pendingMockupIdsRef.current.delete(result.mockupId);
|
|
6069
6207
|
if (wasPending) {
|
|
6070
6208
|
console.log(`[RTP] Cleared pending for ${result.mockupId} \u2014 url cached`);
|
|
6071
6209
|
}
|
|
6210
|
+
if (result.placement) clearedPlacements.add(result.placement);
|
|
6211
|
+
});
|
|
6212
|
+
if (clearedPlacements.size > 0) {
|
|
6213
|
+
clearedPlacements.forEach((p) => pendingPlacementsRef.current.delete(p));
|
|
6072
6214
|
} else {
|
|
6073
|
-
|
|
6215
|
+
pendingPlacementsRef.current.clear();
|
|
6074
6216
|
}
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
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;
|
|
6084
6293
|
}
|
|
6085
|
-
|
|
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 = [];
|
|
6086
6296
|
rawMockupResults.forEach((result) => {
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
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 });
|
|
6093
6316
|
}
|
|
6094
6317
|
});
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
}
|
|
6099
|
-
}, [rawMockupResults]);
|
|
6318
|
+
deliverResults(displayable);
|
|
6319
|
+
maybeFlipCrop();
|
|
6320
|
+
}, [rawMockupResults, deliverResults, maybeFlipCrop, pumpCompositeQueue]);
|
|
6100
6321
|
const sendCanvasBlob = (0, import_react25.useCallback)(
|
|
6101
6322
|
(placement, blob, mockupCount, baseThrottleMs) => {
|
|
6102
6323
|
blobQueuedGenRef.current = pipelineGenRef.current;
|
|
@@ -6155,10 +6376,13 @@ function RealtimeProvider({
|
|
|
6155
6376
|
configSentRef.current = false;
|
|
6156
6377
|
lastSentVariantIdRef.current = null;
|
|
6157
6378
|
lastSentBgRef.current = void 0;
|
|
6379
|
+
lastSentSpreadCropsRef.current = "null";
|
|
6158
6380
|
lastSentColorsRef.current = {};
|
|
6381
|
+
invalidateBackgrounds();
|
|
6382
|
+
lastConfigBaseRef.current = null;
|
|
6159
6383
|
};
|
|
6160
6384
|
}
|
|
6161
|
-
}, [isEnabled, connect, disconnect, wsUrl]);
|
|
6385
|
+
}, [isEnabled, connect, disconnect, wsUrl, invalidateBackgrounds]);
|
|
6162
6386
|
(0, import_react25.useEffect)(() => {
|
|
6163
6387
|
if (rawMockupResults.length > 0 && isPendingMockupsRef.current) {
|
|
6164
6388
|
isPendingMockupsRef.current = false;
|
|
@@ -6177,9 +6401,14 @@ function RealtimeProvider({
|
|
|
6177
6401
|
const isFirstSend = !configSentRef.current;
|
|
6178
6402
|
const variantChanged = lastSentVariantIdRef.current !== null && lastSentVariantIdRef.current !== variantId;
|
|
6179
6403
|
const bgChanged = configSentRef.current && lastSentBgRef.current !== mockupBg;
|
|
6180
|
-
|
|
6404
|
+
const spreadCropsJson = JSON.stringify(spreadCropsRef.current ?? null);
|
|
6405
|
+
const spreadCropsChanged = configSentRef.current && lastSentSpreadCropsRef.current !== spreadCropsJson;
|
|
6406
|
+
if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
|
|
6181
6407
|
return;
|
|
6182
6408
|
}
|
|
6409
|
+
if (variantChanged || bgChanged) {
|
|
6410
|
+
invalidateBackgrounds();
|
|
6411
|
+
}
|
|
6183
6412
|
const mockupIds = [];
|
|
6184
6413
|
if (product.mockups && Array.isArray(product.mockups)) {
|
|
6185
6414
|
product.mockups.forEach((mockup) => {
|
|
@@ -6195,6 +6424,7 @@ function RealtimeProvider({
|
|
|
6195
6424
|
shop,
|
|
6196
6425
|
width: mockupWidth,
|
|
6197
6426
|
...mockupBg ? { bg: mockupBg } : {},
|
|
6427
|
+
...spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {},
|
|
6198
6428
|
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
6199
6429
|
} : {
|
|
6200
6430
|
productId,
|
|
@@ -6204,6 +6434,7 @@ function RealtimeProvider({
|
|
|
6204
6434
|
seal: "bypass-sig-for-k6-load-test",
|
|
6205
6435
|
width: mockupWidth,
|
|
6206
6436
|
...mockupBg ? { bg: mockupBg } : {},
|
|
6437
|
+
...spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {},
|
|
6207
6438
|
placementSettings: { ...placementSettingsOverridesRef.current }
|
|
6208
6439
|
};
|
|
6209
6440
|
if (variantChanged) {
|
|
@@ -6212,10 +6443,14 @@ function RealtimeProvider({
|
|
|
6212
6443
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(p.label));
|
|
6213
6444
|
});
|
|
6214
6445
|
}
|
|
6215
|
-
|
|
6446
|
+
lastConfigBaseRef.current = configPayload;
|
|
6447
|
+
sendConfigRaw(
|
|
6448
|
+
croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
|
|
6449
|
+
);
|
|
6216
6450
|
configSentRef.current = true;
|
|
6217
6451
|
lastSentVariantIdRef.current = variantId;
|
|
6218
6452
|
lastSentBgRef.current = mockupBg;
|
|
6453
|
+
lastSentSpreadCropsRef.current = spreadCropsJson;
|
|
6219
6454
|
setTimeout(() => {
|
|
6220
6455
|
const colorPlacements = placements.filter((p) => p.type === "color");
|
|
6221
6456
|
colorPlacements.forEach((p) => {
|
|
@@ -6233,8 +6468,10 @@ function RealtimeProvider({
|
|
|
6233
6468
|
productContext?.selection,
|
|
6234
6469
|
sendConfigRaw,
|
|
6235
6470
|
sendColorBlobTracked,
|
|
6471
|
+
invalidateBackgrounds,
|
|
6236
6472
|
mockupWidth,
|
|
6237
|
-
mockupBg
|
|
6473
|
+
mockupBg,
|
|
6474
|
+
spreadCropsVersion
|
|
6238
6475
|
]);
|
|
6239
6476
|
(0, import_react25.useEffect)(() => {
|
|
6240
6477
|
if (!isEnabled || !isConnected || !isConfigured || !productContext?.product)
|
|
@@ -6261,6 +6498,15 @@ function RealtimeProvider({
|
|
|
6261
6498
|
productContext?.selection,
|
|
6262
6499
|
sendColorBlobTracked
|
|
6263
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
|
+
);
|
|
6264
6510
|
const updatePlacementSettingsWrapped = (0, import_react25.useCallback)(
|
|
6265
6511
|
(settings) => {
|
|
6266
6512
|
placementSettingsOverridesRef.current = settings;
|
|
@@ -6268,6 +6514,17 @@ function RealtimeProvider({
|
|
|
6268
6514
|
},
|
|
6269
6515
|
[updatePlacementSettingsRaw]
|
|
6270
6516
|
);
|
|
6517
|
+
const setSpreadCrops = (0, import_react25.useCallback)(
|
|
6518
|
+
(crops) => {
|
|
6519
|
+
const next = crops && Object.keys(crops).length > 0 ? crops : void 0;
|
|
6520
|
+
if (JSON.stringify(next ?? null) === JSON.stringify(spreadCropsRef.current ?? null)) {
|
|
6521
|
+
return;
|
|
6522
|
+
}
|
|
6523
|
+
spreadCropsRef.current = next;
|
|
6524
|
+
setSpreadCropsVersion((v) => v + 1);
|
|
6525
|
+
},
|
|
6526
|
+
[]
|
|
6527
|
+
);
|
|
6271
6528
|
const enableRealtime = (0, import_react25.useCallback)(() => {
|
|
6272
6529
|
setIsEnabled(true);
|
|
6273
6530
|
}, []);
|
|
@@ -6449,28 +6706,6 @@ function RealtimeProvider({
|
|
|
6449
6706
|
const isMockupLoading = (0, import_react25.useCallback)((mockupId) => {
|
|
6450
6707
|
return pendingMockupIdsRef.current.has(mockupId);
|
|
6451
6708
|
}, []);
|
|
6452
|
-
const registerBlobUrl = (0, import_react25.useCallback)((blobOrUrl, placementName) => {
|
|
6453
|
-
let url;
|
|
6454
|
-
if (blobOrUrl instanceof Blob) {
|
|
6455
|
-
if (registeredBlobUrlsRef.current.size >= MAX_CONCURRENT_BLOBS) {
|
|
6456
|
-
const oldest = registeredBlobUrlsRef.current.values().next().value;
|
|
6457
|
-
if (oldest) {
|
|
6458
|
-
URL.revokeObjectURL(oldest);
|
|
6459
|
-
registeredBlobUrlsRef.current.delete(oldest);
|
|
6460
|
-
mockupUrlCacheRef.current.forEach((value, key) => {
|
|
6461
|
-
if (value.url === oldest) {
|
|
6462
|
-
mockupUrlCacheRef.current.delete(key);
|
|
6463
|
-
}
|
|
6464
|
-
});
|
|
6465
|
-
}
|
|
6466
|
-
}
|
|
6467
|
-
url = URL.createObjectURL(blobOrUrl);
|
|
6468
|
-
registeredBlobUrlsRef.current.add(url);
|
|
6469
|
-
} else {
|
|
6470
|
-
url = blobOrUrl;
|
|
6471
|
-
}
|
|
6472
|
-
return url;
|
|
6473
|
-
}, []);
|
|
6474
6709
|
const subscribeMockupResultsRef = (0, import_react25.useRef)(subscribeMockupResults);
|
|
6475
6710
|
const subscribeMockupResultByIdRef = (0, import_react25.useRef)(subscribeMockupResultById);
|
|
6476
6711
|
const subscribePendingBlobRef = (0, import_react25.useRef)(subscribePendingBlob);
|
|
@@ -6551,8 +6786,9 @@ function RealtimeProvider({
|
|
|
6551
6786
|
disableRealtime,
|
|
6552
6787
|
sendCanvasBlob,
|
|
6553
6788
|
sendCanvasState,
|
|
6554
|
-
updateMockupIds:
|
|
6789
|
+
updateMockupIds: updateMockupIdsGuarded,
|
|
6555
6790
|
updatePlacementSettings: updatePlacementSettingsWrapped,
|
|
6791
|
+
setSpreadCrops,
|
|
6556
6792
|
getMockupResultsImmediate: () => mockupResultsRef.current,
|
|
6557
6793
|
subscribeMockupResults: stableSubscribeMockupResults,
|
|
6558
6794
|
subscribeMockupResultById: stableSubscribeMockupResultById,
|
|
@@ -6587,8 +6823,9 @@ function RealtimeProvider({
|
|
|
6587
6823
|
disableRealtime,
|
|
6588
6824
|
sendCanvasBlob,
|
|
6589
6825
|
sendCanvasState,
|
|
6590
|
-
|
|
6826
|
+
updateMockupIdsGuarded,
|
|
6591
6827
|
updatePlacementSettingsWrapped,
|
|
6828
|
+
setSpreadCrops,
|
|
6592
6829
|
stableSubscribeMockupResults,
|
|
6593
6830
|
stableSubscribeMockupResultById,
|
|
6594
6831
|
notifyPendingExport,
|
|
@@ -10063,7 +10300,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10063
10300
|
// Quick crossfade
|
|
10064
10301
|
shimmerDelay = 200,
|
|
10065
10302
|
shimmerFadeDuration = 500,
|
|
10066
|
-
darkShimmerFadeInDuration = 300
|
|
10303
|
+
darkShimmerFadeInDuration = 300,
|
|
10304
|
+
emptyText = "No artwork",
|
|
10305
|
+
errorText = "Error loading",
|
|
10306
|
+
unavailableText = "Preview unavailable"
|
|
10067
10307
|
}) {
|
|
10068
10308
|
const context = useProductOptional();
|
|
10069
10309
|
const designContext = useDesignOptional();
|
|
@@ -10508,7 +10748,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10508
10748
|
)
|
|
10509
10749
|
}
|
|
10510
10750
|
),
|
|
10511
|
-
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children:
|
|
10751
|
+
/* @__PURE__ */ (0, import_jsx_runtime61.jsx)("p", { className: "text-xs", children: emptyText })
|
|
10512
10752
|
] })
|
|
10513
10753
|
}
|
|
10514
10754
|
);
|
|
@@ -10519,7 +10759,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10519
10759
|
{
|
|
10520
10760
|
className: `bg-red-50 flex items-center justify-center ${className || ""}`,
|
|
10521
10761
|
style,
|
|
10522
|
-
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 }) })
|
|
10523
10763
|
}
|
|
10524
10764
|
);
|
|
10525
10765
|
}
|
|
@@ -10538,7 +10778,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10538
10778
|
crossOrigin: "anonymous",
|
|
10539
10779
|
className: "absolute inset-0 w-full h-full object-cover",
|
|
10540
10780
|
draggable,
|
|
10541
|
-
src:
|
|
10781
|
+
src: renderedUrl,
|
|
10542
10782
|
loading: "eager",
|
|
10543
10783
|
fetchPriority: "high",
|
|
10544
10784
|
onClick,
|
|
@@ -10554,7 +10794,8 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10554
10794
|
onError: () => {
|
|
10555
10795
|
if (renderedUrl && retriedUrlRef.current !== renderedUrl) {
|
|
10556
10796
|
retriedUrlRef.current = renderedUrl;
|
|
10557
|
-
|
|
10797
|
+
void fetch(renderedUrl, { mode: "cors", cache: "reload", credentials: "omit" }).catch(() => {
|
|
10798
|
+
}).then(() => setRetryNonce((n) => n + 1));
|
|
10558
10799
|
return;
|
|
10559
10800
|
}
|
|
10560
10801
|
setImgFailed(true);
|
|
@@ -10568,9 +10809,10 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10568
10809
|
);
|
|
10569
10810
|
}
|
|
10570
10811
|
}
|
|
10571
|
-
}
|
|
10812
|
+
},
|
|
10813
|
+
retryNonce
|
|
10572
10814
|
),
|
|
10573
|
-
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 }) }),
|
|
10574
10816
|
realtimeContext?.renderError && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10575
10817
|
"div",
|
|
10576
10818
|
{
|
|
@@ -12060,7 +12302,10 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12060
12302
|
onZoomChange,
|
|
12061
12303
|
stickyHeroContext,
|
|
12062
12304
|
mockupWidth,
|
|
12063
|
-
onMockupUrlGenerated
|
|
12305
|
+
onMockupUrlGenerated,
|
|
12306
|
+
renderImageAction,
|
|
12307
|
+
errorText,
|
|
12308
|
+
unavailableText
|
|
12064
12309
|
}) {
|
|
12065
12310
|
const [activeIndex, setActiveIndex] = (0, import_react56.useState)(currentIndex);
|
|
12066
12311
|
const [touchStart, setTouchStart] = (0, import_react56.useState)(
|
|
@@ -12594,34 +12839,52 @@ var MobileProductCarousel = (0, import_react56.memo)(function MobileProductCarou
|
|
|
12594
12839
|
children: images.map((image, index) => {
|
|
12595
12840
|
const DECODE_WINDOW = 1;
|
|
12596
12841
|
const isWithinDecodeWindow = Math.abs(index - activeIndex) <= DECODE_WINDOW;
|
|
12597
|
-
return /* @__PURE__ */ (0, import_jsx_runtime74.
|
|
12842
|
+
return /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)(
|
|
12598
12843
|
"div",
|
|
12599
12844
|
{
|
|
12600
12845
|
className: "w-full h-full flex-shrink-0 relative select-none",
|
|
12601
|
-
children:
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
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
|
+
]
|
|
12625
12888
|
},
|
|
12626
12889
|
index
|
|
12627
12890
|
);
|